Compare commits
No commits in common. "cf9a02ae38e8ea6ad13b0cd6f73cf718049443b9" and "ae3749bab92ffa9094eb2d218147a95bc3aa3109" have entirely different histories.
cf9a02ae38
...
ae3749bab9
|
|
@ -36,28 +36,9 @@ jobs:
|
||||||
name: release-bundle
|
name: release-bundle
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|
||||||
browser-journey:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build-release
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with: { python-version: "3.11" }
|
|
||||||
- name: Download assembled release bundle
|
|
||||||
uses: actions/download-artifact@v3
|
|
||||||
with:
|
|
||||||
name: release-bundle
|
|
||||||
path: dist
|
|
||||||
- name: Install browser test dependencies
|
|
||||||
run: |
|
|
||||||
pip install -r requirements-e2e.txt
|
|
||||||
python3 -m playwright install --with-deps chromium
|
|
||||||
- name: Exercise mobile offline filing from the release artifact
|
|
||||||
run: python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py -q
|
|
||||||
|
|
||||||
release-candidate:
|
release-candidate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [lint, build-release, browser-journey]
|
needs: [lint, build-release]
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
|
||||||
|
|
@ -5055,7 +5055,7 @@
|
||||||
const evidence = await createIssueAttachmentController.serialize();
|
const evidence = await createIssueAttachmentController.serialize();
|
||||||
const captureDraft = currentIssueCaptureDraft();
|
const captureDraft = currentIssueCaptureDraft();
|
||||||
Object.assign(captureDraft, Array.isArray(evidence) ? {attachments:evidence} : {attachment:evidence});
|
Object.assign(captureDraft, Array.isArray(evidence) ? {attachments:evidence} : {attachment:evidence});
|
||||||
if (showDraftCapacityDialog(unfiledCaptures, qs)) return;
|
if (showDraftCapacityDialog(unfiledCaptures)) return;
|
||||||
const savedCapture = await unfiledCaptures.save(captureDraft);
|
const savedCapture = await unfiledCaptures.save(captureDraft);
|
||||||
if (rUC) {
|
if (rUC) {
|
||||||
await unfiledCaptures.completeResume(rUC);
|
await unfiledCaptures.completeResume(rUC);
|
||||||
|
|
@ -5080,10 +5080,7 @@
|
||||||
qs('#create-issue-title').focus();
|
qs('#create-issue-title').focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
bindDraftCapacityDialog({
|
bindDraftCapacityDialog();
|
||||||
qs, saveIssueCaptureDraft, closeCreateIssueSheet, mobileTaskDock, refreshMyWorkView,
|
|
||||||
unfiledCaptures, currentIssueCaptureDraft, createIssueAttachmentController, issueCapture,
|
|
||||||
});
|
|
||||||
qs('#use-shared-content').addEventListener('click', () => {
|
qs('#use-shared-content').addEventListener('click', () => {
|
||||||
if (issueCapture.pendingFollowUp()) issueCapture.acceptFollowUp();
|
if (issueCapture.pendingFollowUp()) issueCapture.acceptFollowUp();
|
||||||
else issueCapture.acceptSharedContent();
|
else issueCapture.acceptSharedContent();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
function showDraftCapacityDialog(captures, qs) {
|
function showDraftCapacityDialog(captures) {
|
||||||
const state = captures.capacity();
|
const state = captures.capacity();
|
||||||
if (!state.full) return false;
|
if (!state.full) return false;
|
||||||
const oldest = state.oldest;
|
const oldest = state.oldest;
|
||||||
|
|
@ -11,10 +11,7 @@ function showDraftCapacityDialog(captures, qs) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindDraftCapacityDialog({
|
function bindDraftCapacityDialog() {
|
||||||
qs, saveIssueCaptureDraft, closeCreateIssueSheet, mobileTaskDock, refreshMyWorkView,
|
|
||||||
unfiledCaptures, currentIssueCaptureDraft, createIssueAttachmentController, issueCapture,
|
|
||||||
}) {
|
|
||||||
qs('#keep-editing-draft').addEventListener('click', () => {
|
qs('#keep-editing-draft').addEventListener('click', () => {
|
||||||
qs('#draft-capacity-sheet').hidden = true;
|
qs('#draft-capacity-sheet').hidden = true;
|
||||||
qs('#save-unfiled-issue').focus();
|
qs('#save-unfiled-issue').focus();
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
-r requirements.txt
|
|
||||||
playwright==1.55.0
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
"""Deterministic Gitea double used by the release-artifact browser journey."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
||||||
from urllib.parse import urlsplit
|
|
||||||
|
|
||||||
|
|
||||||
REPOSITORY = {
|
|
||||||
"id": 7,
|
|
||||||
"name": "mobile",
|
|
||||||
"full_name": "acme/mobile",
|
|
||||||
"description": "Mobile filing acceptance repository",
|
|
||||||
"html_url": "http://127.0.0.1/acme/mobile",
|
|
||||||
"owner": {"login": "acme"},
|
|
||||||
}
|
|
||||||
USER = {"id": 1, "login": "timmy", "full_name": "Timmy"}
|
|
||||||
|
|
||||||
|
|
||||||
class FakeGiteaServer(ThreadingHTTPServer):
|
|
||||||
def __init__(self, address: tuple[str, int]):
|
|
||||||
super().__init__(address, FakeGiteaHandler)
|
|
||||||
self.created_issues: list[dict] = []
|
|
||||||
|
|
||||||
|
|
||||||
class FakeGiteaHandler(BaseHTTPRequestHandler):
|
|
||||||
server: FakeGiteaServer
|
|
||||||
|
|
||||||
def log_message(self, _format: str, *_args: object) -> None:
|
|
||||||
return
|
|
||||||
|
|
||||||
def _json(self, status: int, payload: object, **headers: str) -> None:
|
|
||||||
body = json.dumps(payload).encode()
|
|
||||||
self.send_response(status)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.send_header("Content-Length", str(len(body)))
|
|
||||||
for name, value in headers.items():
|
|
||||||
self.send_header(name, value)
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
|
|
||||||
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
|
|
||||||
path = urlsplit(self.path).path
|
|
||||||
if path == "/api/v1/user":
|
|
||||||
self._json(200, USER)
|
|
||||||
elif path == "/api/v1/user/repos":
|
|
||||||
self._json(200, [REPOSITORY], **{"X-Total-Count": "1"})
|
|
||||||
elif path in {"/api/v1/repos/acme/mobile", "/api/v1/repos/acme/mobile/"}:
|
|
||||||
self._json(200, REPOSITORY)
|
|
||||||
elif path == "/api/v1/repos/search":
|
|
||||||
self._json(200, {"data": [REPOSITORY], "ok": True})
|
|
||||||
elif path.startswith("/api/v1/"):
|
|
||||||
self._json(200, [], **{"X-Total-Count": "0"})
|
|
||||||
else:
|
|
||||||
self._json(404, {"message": "not found"})
|
|
||||||
|
|
||||||
def do_POST(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
|
|
||||||
path = urlsplit(self.path).path
|
|
||||||
length = int(self.headers.get("Content-Length", "0"))
|
|
||||||
payload = json.loads(self.rfile.read(length) or b"{}")
|
|
||||||
if path != "/api/v1/repos/acme/mobile/issues":
|
|
||||||
self._json(404, {"message": "not found"})
|
|
||||||
return
|
|
||||||
self.server.created_issues.append(payload)
|
|
||||||
assignee = payload.get("assignee")
|
|
||||||
self._json(
|
|
||||||
201,
|
|
||||||
{
|
|
||||||
"id": 101,
|
|
||||||
"number": 41,
|
|
||||||
"title": payload.get("title", ""),
|
|
||||||
"body": payload.get("body", ""),
|
|
||||||
"state": "open",
|
|
||||||
"html_url": "http://127.0.0.1/acme/mobile/issues/41",
|
|
||||||
"assignees": [{"login": assignee}] if assignee else [],
|
|
||||||
"labels": [],
|
|
||||||
"milestone": None,
|
|
||||||
"due_date": payload.get("due_date"),
|
|
||||||
"updated_at": "2026-08-15T12:00:00Z",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
@ -1,251 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import ssl
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tarfile
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import urllib.request
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
pytest.importorskip("playwright.sync_api")
|
|
||||||
from playwright.sync_api import Page, expect, sync_playwright
|
|
||||||
|
|
||||||
from fake_gitea import FakeGiteaServer
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
TITLE = "Offline artifact journey 863"
|
|
||||||
BODY = "Captured on a phone, retained offline, delivered exactly once."
|
|
||||||
ACCESS_TOKEN = "artifact-browser-access-token-863"
|
|
||||||
|
|
||||||
|
|
||||||
def free_port() -> int:
|
|
||||||
with socket.socket() as sock:
|
|
||||||
sock.bind(("127.0.0.1", 0))
|
|
||||||
return int(sock.getsockname()[1])
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_health(url: str, process: subprocess.Popen[str]) -> None:
|
|
||||||
deadline = time.monotonic() + 20
|
|
||||||
tls = ssl._create_unverified_context()
|
|
||||||
while time.monotonic() < deadline:
|
|
||||||
if process.poll() is not None:
|
|
||||||
stdout, stderr = process.communicate()
|
|
||||||
raise AssertionError(f"release server exited early\nstdout:\n{stdout}\nstderr:\n{stderr}")
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(url, timeout=0.5, context=tls) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
return
|
|
||||||
except OSError:
|
|
||||||
time.sleep(0.05)
|
|
||||||
raise AssertionError("release server did not become healthy")
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def release_server(archive: Path, tmp_path: Path, gitea_url: str):
|
|
||||||
release_root = tmp_path / "release"
|
|
||||||
release_root.mkdir()
|
|
||||||
with tarfile.open(archive, "r:gz") as bundle:
|
|
||||||
bundle.extractall(release_root, filter="data")
|
|
||||||
assert (release_root / "release-manifest.json").is_file()
|
|
||||||
assert not (release_root / "tests").exists(), "journey must run the packaged artifact, not checkout code"
|
|
||||||
|
|
||||||
port = free_port()
|
|
||||||
state_dir = tmp_path / "state"
|
|
||||||
state_dir.mkdir()
|
|
||||||
key_file = tmp_path / "localhost.key"
|
|
||||||
certificate_file = tmp_path / "localhost.crt"
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
"openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
||||||
"-keyout", str(key_file), "-out", str(certificate_file), "-days", "1",
|
|
||||||
"-subj", "/CN=127.0.0.1", "-addext", "subjectAltName=IP:127.0.0.1",
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
origin = f"https://127.0.0.1:{port}"
|
|
||||||
env = {
|
|
||||||
**os.environ,
|
|
||||||
"PYTHONPATH": str(release_root),
|
|
||||||
"GITEA_URL": gitea_url,
|
|
||||||
"GITEA_TOKEN": "deterministic-e2e-token",
|
|
||||||
"STACKCHAIN_DASHBOARD_AUTH_MODE": "operator",
|
|
||||||
"STACKCHAIN_DASHBOARD_ACCESS_TOKEN": ACCESS_TOKEN,
|
|
||||||
"STACKCHAIN_DASHBOARD_SESSION_SECRET": "artifact-browser-independent-session-secret-863",
|
|
||||||
"STACKCHAIN_DASHBOARD_PUBLIC_ORIGIN": origin,
|
|
||||||
"STACKCHAIN_STATE_DIR": str(tmp_path / "state"),
|
|
||||||
}
|
|
||||||
process = subprocess.Popen(
|
|
||||||
[
|
|
||||||
sys.executable, "-m", "uvicorn", "src.main:app",
|
|
||||||
"--host", "127.0.0.1", "--port", str(port),
|
|
||||||
"--ssl-keyfile", str(key_file), "--ssl-certfile", str(certificate_file),
|
|
||||||
],
|
|
||||||
cwd=release_root,
|
|
||||||
env=env,
|
|
||||||
text=True,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
wait_for_health(origin + "/healthz", process)
|
|
||||||
yield origin
|
|
||||||
finally:
|
|
||||||
process.terminate()
|
|
||||||
try:
|
|
||||||
process.communicate(timeout=10)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
process.kill()
|
|
||||||
process.communicate()
|
|
||||||
|
|
||||||
|
|
||||||
def indexed_issue_records(page: Page) -> list[dict]:
|
|
||||||
return page.evaluate(
|
|
||||||
"""async () => {
|
|
||||||
const db = await new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open('stackchain-background-outbox-v1', 1);
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
const request = db.transaction('issues', 'readonly').objectStore('issues').getAll();
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
|
||||||
request.onerror = () => reject(request.error);
|
|
||||||
});
|
|
||||||
}"""
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_release_artifact_files_one_mobile_issue_exactly_once_after_offline_reload(tmp_path: Path):
|
|
||||||
archives = sorted((ROOT / "dist").glob("stackchain-dashboard-*.tar.gz"))
|
|
||||||
assert len(archives) == 1, "browser job must download exactly one assembled release archive"
|
|
||||||
|
|
||||||
fake = FakeGiteaServer(("127.0.0.1", 0))
|
|
||||||
fake_thread = threading.Thread(target=fake.serve_forever, daemon=True)
|
|
||||||
fake_thread.start()
|
|
||||||
fake_url = f"http://127.0.0.1:{fake.server_port}"
|
|
||||||
browser_errors: list[str] = []
|
|
||||||
failed_responses: list[str] = []
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
with release_server(archives[0], tmp_path, fake_url) as origin, sync_playwright() as playwright:
|
|
||||||
browser = playwright.chromium.launch(args=["--ignore-certificate-errors"])
|
|
||||||
context = browser.new_context(
|
|
||||||
viewport={"width": 390, "height": 844},
|
|
||||||
service_workers="allow",
|
|
||||||
ignore_https_errors=True,
|
|
||||||
)
|
|
||||||
page = context.new_page()
|
|
||||||
page.on("pageerror", lambda error: browser_errors.append(error.stack or str(error)))
|
|
||||||
page.on("console", lambda message: browser_errors.append(message.text) if message.type == "error" else None)
|
|
||||||
page.on("response", lambda response: failed_responses.append(
|
|
||||||
f"{response.status} {response.url}"
|
|
||||||
) if response.status >= 400 else None)
|
|
||||||
page.goto(origin + "/", wait_until="networkidle")
|
|
||||||
expect(page.locator("#sign-in")).to_be_visible()
|
|
||||||
page.locator('input[name="device_label"]').fill("Release journey phone")
|
|
||||||
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
|
|
||||||
page.locator("#submit-sign-in").click()
|
|
||||||
page.wait_for_url(origin + "/", wait_until="networkidle")
|
|
||||||
new_action = page.locator('[data-mobile-task="new"]')
|
|
||||||
expect(new_action).to_be_visible()
|
|
||||||
service_worker_ready = page.evaluate(
|
|
||||||
"""() => Promise.race([
|
|
||||||
navigator.serviceWorker.ready.then(() => true),
|
|
||||||
new Promise(resolve => setTimeout(() => resolve(false), 10000)),
|
|
||||||
])"""
|
|
||||||
)
|
|
||||||
assert service_worker_ready, (
|
|
||||||
"release artifact did not register its service worker; browser errors=" + repr(browser_errors)
|
|
||||||
+ "; failed responses=" + repr(failed_responses)
|
|
||||||
)
|
|
||||||
if not page.evaluate("() => navigator.serviceWorker.controller !== null"):
|
|
||||||
page.reload(wait_until="networkidle")
|
|
||||||
new_action = page.locator('[data-mobile-task="new"]')
|
|
||||||
assert page.evaluate("() => navigator.serviceWorker.controller !== null")
|
|
||||||
|
|
||||||
new_action.click()
|
|
||||||
page.locator("#create-issue-title").fill(TITLE)
|
|
||||||
page.locator("#create-issue-body").fill(BODY)
|
|
||||||
page.locator("#file-new-issue").click()
|
|
||||||
expect(page.locator("#create-issue-filing")).to_be_visible()
|
|
||||||
page.locator("#create-issue-repository").select_option("acme/mobile")
|
|
||||||
expect(page.locator("#submit-new-issue")).to_be_enabled()
|
|
||||||
|
|
||||||
context.set_offline(True)
|
|
||||||
page.locator("#submit-new-issue").click()
|
|
||||||
expect(page.locator("#issue-filing-review")).to_be_visible()
|
|
||||||
page.locator("#confirm-issue-filing").click()
|
|
||||||
page.wait_for_timeout(1_000)
|
|
||||||
filing_statuses = page.locator(
|
|
||||||
"#my-work-action-status, #create-issue-status, #issue-filing-review-status"
|
|
||||||
).all_inner_texts()
|
|
||||||
assert any(
|
|
||||||
marker in " ".join(filing_statuses).lower()
|
|
||||||
for marker in ("queued", "saved for", "background delivery")
|
|
||||||
), (filing_statuses, browser_errors, failed_responses)
|
|
||||||
|
|
||||||
local_record = json.loads(page.evaluate("localStorage.getItem('stackchain.issue-outbox.v1')"))
|
|
||||||
assert [(item["title"], item["body"], item["repository"]) for item in local_record["items"]] == [
|
|
||||||
(TITLE, BODY, "acme/mobile")
|
|
||||||
]
|
|
||||||
durable = indexed_issue_records(page)
|
|
||||||
assert [(item["title"], item["body"], item["repository"]) for item in durable] == [
|
|
||||||
(TITLE, BODY, "acme/mobile")
|
|
||||||
]
|
|
||||||
operation_id = durable[0]["operationId"]
|
|
||||||
assert operation_id and operation_id == local_record["items"][0]["operationId"]
|
|
||||||
assert fake.created_issues == []
|
|
||||||
|
|
||||||
page.reload(wait_until="domcontentloaded")
|
|
||||||
expect(page.locator('[data-mobile-task="new"]')).to_be_visible()
|
|
||||||
durable_after_reload = indexed_issue_records(page)
|
|
||||||
assert [(item["title"], item["body"]) for item in durable_after_reload] == [(TITLE, BODY)]
|
|
||||||
assert durable_after_reload[0]["operationId"] == operation_id
|
|
||||||
assert fake.created_issues == []
|
|
||||||
|
|
||||||
browser_errors.clear() # Chromium reports expected network errors while the context is offline.
|
|
||||||
failed_responses.clear()
|
|
||||||
context.set_offline(False)
|
|
||||||
page.evaluate("window.dispatchEvent(new Event('online'))")
|
|
||||||
for _ in range(80):
|
|
||||||
if fake.created_issues:
|
|
||||||
break
|
|
||||||
page.wait_for_timeout(250)
|
|
||||||
assert fake.created_issues == [{"title": TITLE, "body": BODY, "assignee": "timmy"}]
|
|
||||||
|
|
||||||
for _ in range(40):
|
|
||||||
durable_completion = indexed_issue_records(page)
|
|
||||||
if durable_completion and all(item.get("status") == "sent" for item in durable_completion):
|
|
||||||
break
|
|
||||||
page.wait_for_timeout(100)
|
|
||||||
assert durable_completion and all(item.get("status") == "sent" for item in durable_completion)
|
|
||||||
page.reload(wait_until="networkidle")
|
|
||||||
page.evaluate("window.dispatchEvent(new Event('online'))")
|
|
||||||
for _ in range(40):
|
|
||||||
completed_local = json.loads(page.evaluate("localStorage.getItem('stackchain.issue-outbox.v1')"))
|
|
||||||
if completed_local["items"] == []:
|
|
||||||
break
|
|
||||||
page.wait_for_timeout(100)
|
|
||||||
assert completed_local["items"] == []
|
|
||||||
page.wait_for_timeout(500)
|
|
||||||
assert len(fake.created_issues) == 1
|
|
||||||
assert browser_errors == []
|
|
||||||
assert failed_responses == []
|
|
||||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
|
||||||
browser.close()
|
|
||||||
finally:
|
|
||||||
fake.shutdown()
|
|
||||||
fake.server_close()
|
|
||||||
fake_thread.join(timeout=5)
|
|
||||||
|
|
@ -21,21 +21,8 @@ def test_release_promotion_waits_for_tests_and_bundle():
|
||||||
text = WORKFLOW.read_text()
|
text = WORKFLOW.read_text()
|
||||||
release = text[text.index(" release-candidate:") :]
|
release = text[text.index(" release-candidate:") :]
|
||||||
|
|
||||||
assert "needs: [lint, build-release, browser-journey]" in release
|
assert "needs: [lint, build-release]" in release
|
||||||
assert "github.event_name == 'push'" in release
|
assert "github.event_name == 'push'" in release
|
||||||
assert "actions/download-artifact@v3" in release
|
assert "actions/download-artifact@v3" in release
|
||||||
assert 'python3 scripts/verify_release.py --input-dir dist --commit "$TARGET"' in release
|
assert 'python3 scripts/verify_release.py --input-dir dist --commit "$TARGET"' in release
|
||||||
assert release.index("sha256sum -c") < release.index("curl --fail-with-body")
|
assert release.index("sha256sum -c") < release.index("curl --fail-with-body")
|
||||||
|
|
||||||
|
|
||||||
def test_release_promotion_waits_for_artifact_mobile_offline_journey():
|
|
||||||
text = WORKFLOW.read_text()
|
|
||||||
browser = text[text.index(" browser-journey:") : text.index(" release-candidate:")]
|
|
||||||
release = text[text.index(" release-candidate:") :]
|
|
||||||
|
|
||||||
assert "needs: build-release" in browser
|
|
||||||
assert "actions/download-artifact@v3" in browser
|
|
||||||
assert "pip install -r requirements-e2e.txt" in browser
|
|
||||||
assert "python3 -m playwright install --with-deps chromium" in browser
|
|
||||||
assert "python3 -m pytest tests/e2e/test_mobile_offline_issue_release.py -q" in browser
|
|
||||||
assert "needs: [lint, build-release, browser-journey]" in release
|
|
||||||
|
|
|
||||||
|
|
@ -739,8 +739,7 @@ async def test_mobile_capture_capacity_requires_an_explicit_preserving_decision(
|
||||||
assert 'id="review-full-drafts"' in html
|
assert 'id="review-full-drafts"' in html
|
||||||
assert 'id="replace-oldest-draft"' in html
|
assert 'id="replace-oldest-draft"' in html
|
||||||
assert 'id="keep-editing-draft"' in html
|
assert 'id="keep-editing-draft"' in html
|
||||||
assert "showDraftCapacityDialog(unfiledCaptures, qs)" in html
|
assert "showDraftCapacityDialog(unfiledCaptures)" in html
|
||||||
assert "bindDraftCapacityDialog({" in html
|
|
||||||
feature = (Path(__file__).parents[1] / "frontend" / "draft-capacity-dialog.js").read_text()
|
feature = (Path(__file__).parents[1] / "frontend" / "draft-capacity-dialog.js").read_text()
|
||||||
assert "const draft = currentIssueCaptureDraft();" in feature
|
assert "const draft = currentIssueCaptureDraft();" in feature
|
||||||
assert "Array.isArray(evidence) ? {attachments:evidence} : {attachment:evidence}" in feature
|
assert "Array.isArray(evidence) ? {attachments:evidence} : {attachment:evidence}" in feature
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user