Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: runner/playwright-report/
# playwright-report/ holds the html report, test-results/ the traces.
path: |
runner/playwright-report/
runner/test-results/
retention-days: 7
if-no-files-found: warn
6 changes: 5 additions & 1 deletion .github/workflows/e2e-live.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: playwright-report-live
path: runner/playwright-report/
# playwright-report/ holds the html report, test-results/ the traces.
path: |
runner/playwright-report/
runner/test-results/
retention-days: 7
if-no-files-found: warn
135 changes: 135 additions & 0 deletions runner/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { expect, type APIRequestContext, type Page } from "@playwright/test";

// Shared helpers for the e2e suite (DEV-2203).
//
// Everything here existed first as copy-paste: `stubShell` and `signIn` were
// declared verbatim in nine specs, the session-cleanup pattern in two, the
// noise filter in one. New specs import from here; existing specs keep their
// local copies and migrate opportunistically — a mass rewrite would churn
// thousands of spec lines for zero behaviour change.
//
// The file is not matched by `*.spec.ts`, so Playwright never collects it.

export const EMAIL = "dev@handsontable.com";

/**
* The deterministic-shell recipe: a stubbed version list, both Sandpack hosts
* aborted (no external bundler, no grid — the shell renders fine without one),
* and the login redirect neutered so a stray click cannot leave the app.
*/
export async function stubShell(page: Page) {
await page.route("**/api/versions", (route) =>
route.fulfill({ json: { latest: "18.0.0", next: "19.0.0-next.1", versions: ["18.0.0", "17.1.0"] } }),
);
await page.route("https://sandpack.codesandbox.io/**", (route) => route.abort());
await page.route("https://sandpack-bundler.codesandbox.io/**", (route) => route.abort());
await page.route("**/broker/login**", (route) => route.abort());
}

/**
* Sign-in faked at the token layer — the app reads `sessionStorage.hot_token`
* and asks the broker who that is. Faking here rather than via `VITE_DEV_USER`
* keeps the production auth path in play (see sidebar-crud.spec.ts for the full
* argument). Never add storage *clears* to an init script: it runs on
* `page.reload()` too and silently defeats persistence tests (AGENTS.md).
*/
export async function signIn(page: Page, email: string = EMAIL) {
await page.addInitScript(() => sessionStorage.setItem("hot_token", "e2e-token"));
await page.route("**/broker/userinfo", (route) => route.fulfill({ json: { email } }));
}

/**
* The visible editor pane. Scoped since T12 (DEV-2169): every open tab keeps
* its own CodeMirror instance mounted, so a bare `.cm-content` trips strict
* mode as soon as a test opens a second file.
*
* For *reading file contents*, prefer `workspaceFiles()` — CodeMirror
* virtualises long documents, so `.cm-content` only holds the lines on screen.
*/
export function activeEditor(page: Page) {
return page.locator('[data-pane-active="true"] .cm-content');
}

/**
* The workspace files, via the `window.__HOT_FILES__` test contract
* (apps/authoring/src/App.tsx). Poll with `expect(...).toPass()` when the
* write you are waiting for rides the Style panel's 250 ms debounce.
*/
export function workspaceFiles(page: Page): Promise<Record<string, string>> {
return page.evaluate(() => {
const hook = (window as unknown as { __HOT_FILES__?: () => Record<string, string> }).__HOT_FILES__;
if (!hook) throw new Error("window.__HOT_FILES__ is not installed — is the app older than DEV-2203?");
return hook();
});
}

/**
* The version and framework pickers are custom listboxes in the preview bar
* (T2, DEV-2156) — `selectOption` does not apply. Open the trigger, click the
* option.
*/
export async function pickFromMenu(page: Page, menu: "Handsontable version" | "Framework", option: string) {
await page.getByRole("button", { name: menu, exact: true }).click();
await page.getByRole("option", { name: option, exact: true }).click();
}

/**
* Wait for the preview to declare itself ready. Readiness comes off
* `data-preview-status` on the preview section (PreviewPane.tsx — a documented
* test contract), never off visible text. For containers, "ready" only means
* the dev server responded; follow up with `expectGridRendered` before
* asserting anything about the demo itself.
*/
export async function previewReady(page: Page, engine: "sandpack" | "container" = "sandpack") {
await expect(page.locator('section[aria-label="Preview"]')).toHaveAttribute("data-preview-status", "ready", {
timeout: engine === "container" ? 240_000 : 120_000,
});
}

/** The rendered grid inside the preview frame — the real functional check. */
export function gridCells(page: Page) {
return page.frameLocator('iframe[title="Demo preview"]').locator(".handsontable .htCore td");
}

export async function expectGridRendered(page: Page) {
await expect
.poll(async () => gridCells(page).count().catch(() => 0), { timeout: 60_000, intervals: [1_000] })
.toBeGreaterThan(0);
}

// Console/page noise that isn't a real break. Extend as live runs surface new
// false positives (same list as starter-matrix.spec.ts).
export const NOISE = [
/non-commercial|evaluation license/i,
/Download the React DevTools/i,
/favicon\.ico/i,
/\[vite\] (connecting|connected)/i,
/ERR_BLOCKED_BY_CLIENT|third-party cookie/i,
];
export const isKnownNoise = (message: string) => NOISE.some((re) => re.test(message));

/**
* Track Tier-2 sessions a test creates so they can be torn down even when the
* test fails: the container pool holds five global slots shared with real
* traffic, and a leaked session squats one for its whole idle window.
*
* const tracked = trackSessions(page);
* try { ... } finally { await tracked.cleanup(request); }
*/
export function trackSessions(page: Page) {
const sessions: { id: string; apiBase: string }[] = [];
page.on("response", async (res) => {
if (res.request().method() === "POST" && /\/api\/session$/.test(res.url()) && res.ok()) {
const body = (await res.json().catch(() => null)) as { sessionId?: string } | null;
if (body?.sessionId) sessions.push({ id: body.sessionId, apiBase: new URL(res.url()).origin });
}
});
return {
sessions,
async cleanup(request: APIRequestContext) {
for (const { id, apiBase } of sessions) {
await request.delete(`${apiBase}/api/session/${id}`).catch(() => {});
}
},
};
}
3 changes: 2 additions & 1 deletion runner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
},
"devDependencies": {
"@playwright/test": "^1.48.0",
"acorn": "^8.18.0",
"acorn": "^8.18.0",
"fflate": "^0.8.2",
"typescript": "~5.6.0"
}
}
5 changes: 4 additions & 1 deletion runner/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export default defineConfig({
expect: { timeout: 15_000 },
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? "github" : "list",
// In CI, "github" alone leaves `playwright-report/` empty — the workflows
// upload that directory on failure, and until DEV-2203 the artifact was a
// no-op. The html reporter fills it; traces land in `test-results/`.
reporter: process.env.CI ? [["list"], ["github"], ["html", { open: "never" }]] : "list",
use: { baseURL, trace: "on-first-retry" },
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
// Start a local preview only when not pointing at an external URL.
Expand Down
3 changes: 3 additions & 0 deletions runner/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading