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
21 changes: 19 additions & 2 deletions e2e/live/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { request } from '@playwright/test';
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { dirname, join } from 'node:path';

/**
* Live-e2e auth: sign in against the real ObjectStack backend (better-auth) and
Expand All @@ -12,7 +12,24 @@
const API = process.env.LIVE_API_URL || 'http://localhost:3000';
const EMAIL = process.env.LIVE_EMAIL || 'admin@objectos.ai';
const PASSWORD = process.env.LIVE_PASSWORD || 'admin123';
const STATE_PATH = 'e2e/live/.auth/state.json';

/**
* Rooted on THIS FILE, never on `process.cwd()` (objectui#9519). A bare
* relative path handed to `writeFileSync` names no root, so it gets the ambient
* one: measured on this tree, a live run launched from `e2e/` really created
* `e2e/e2e/live/.auth/state.json`, one directory below where the specs — which
* root their READ on their own file since objectui#9188 — go looking.
*
* Bare `import.meta.url` taken apart by hand: the spelling PR #7796 landed and
* PR #7806 reused. Not `new URL(rel, import.meta.url)` — this repo prescribes
* one spelling, on grounds of spelling uniqueness (objectui#9191).
*/
const SELF_DEPTH_BELOW_REPO_ROOT = 3; // e2e / live / this file
const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname)
.split('/')
.slice(0, -SELF_DEPTH_BELOW_REPO_ROOT)
.join('/');
const STATE_PATH = join(REPO_ROOT, 'e2e/live/.auth/state.json');

export default async function globalSetup() {
const ctx = await request.newContext();
Expand All @@ -22,7 +39,7 @@
data: { email: EMAIL, password: PASSWORD },
headers: { 'Content-Type': 'application/json' },
});
} catch (e: any) {

Check warning on line 42 in e2e/live/global-setup.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
throw new Error(
`Live backend unreachable at ${API} (${e?.message}). Start it (e.g. \`objectstack serve --dev\` in examples/app-showcase) before running live e2e.`,
{ cause: e },
Expand Down
21 changes: 20 additions & 1 deletion playwright.import-console.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig, devices } from '@playwright/test';
import { join } from 'node:path';

/**
* REAL-CONSOLE import-wizard E2E — the most faithful "how a user actually does
Expand Down Expand Up @@ -38,6 +39,24 @@ import { defineConfig, devices } from '@playwright/test';
*/
const APP = process.env.LIVE_APP_URL || 'http://localhost:5180';

/**
* `storageState` is a string this config hands to Playwright, and Playwright
* resolves a relative one against the PROCESS CWD — measured on this tree with
* playwright 1.62.1: a run launched from a subdirectory loaded the state file
* under that subdirectory and ignored the one beside the config (it failed with
* `Error reading storage state from state/probe-state.json` when only the
* config-dir copy existed). `testDir` and `globalSetup` above are config-dir
* rooted, this option is not — so it is rooted here, on this file, the same way
* the specs root their read (objectui#9188) and the global setup roots its
* write (objectui#9519).
*/
const SELF_DEPTH_BELOW_REPO_ROOT = 1; // this file, at the repo root
const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname)
.split('/')
.slice(0, -SELF_DEPTH_BELOW_REPO_ROOT)
.join('/');
const STATE_PATH = join(REPO_ROOT, 'e2e/live/.auth/state.json');

export default defineConfig({
testDir: './e2e/import-console',
fullyParallel: false,
Expand All @@ -47,7 +66,7 @@ export default defineConfig({
globalSetup: './e2e/live/global-setup.ts',
use: {
baseURL: APP,
storageState: 'e2e/live/.auth/state.json',
storageState: STATE_PATH,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
Expand Down
21 changes: 20 additions & 1 deletion playwright.live.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig, devices } from '@playwright/test';
import { join } from 'node:path';

/**
* LIVE e2e — drives the REAL running stack (console dev server + ObjectStack
Expand All @@ -23,6 +24,24 @@ import { defineConfig, devices } from '@playwright/test';
*/
const APP = process.env.LIVE_APP_URL || 'http://localhost:5180';

/**
* `storageState` is a string this config hands to Playwright, and Playwright
* resolves a relative one against the PROCESS CWD — measured on this tree with
* playwright 1.62.1: a run launched from a subdirectory loaded the state file
* under that subdirectory and ignored the one beside the config (it failed with
* `Error reading storage state from state/probe-state.json` when only the
* config-dir copy existed). `testDir` and `globalSetup` above are config-dir
* rooted, this option is not — so it is rooted here, on this file, the same way
* the specs root their read (objectui#9188) and the global setup roots its
* write (objectui#9519).
*/
const SELF_DEPTH_BELOW_REPO_ROOT = 1; // this file, at the repo root
const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname)
.split('/')
.slice(0, -SELF_DEPTH_BELOW_REPO_ROOT)
.join('/');
const STATE_PATH = join(REPO_ROOT, 'e2e/live/.auth/state.json');

export default defineConfig({
testDir: './e2e/live',
fullyParallel: false,
Expand All @@ -32,7 +51,7 @@ export default defineConfig({
globalSetup: './e2e/live/global-setup.ts',
use: {
baseURL: APP,
storageState: 'e2e/live/.auth/state.json',
storageState: STATE_PATH,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
Expand Down
64 changes: 64 additions & 0 deletions scripts/__tests__/live-e2e-storage-state-roots-9519.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* The live-e2e storage state is written by one file, named by two configs and
* read by the specs — and all four must agree on the SAME tree.
*
* ## Why this pin exists where the gate cannot reach
*
* `scripts/check-test-path-roots.mjs` rejects a cwd-rooted filesystem path, but
* its population is `TEST_FILE` — `*.test.*` / `*.spec.*`. A `global-setup.ts`
* and a `*.config.ts` match neither, so the gate's registry could reach zero
* (objectui#9188 repaired the last registered entry, the READ in
* `inline-edit-polish-2572.spec.ts`) while two instances of the very same class
* sat one directory away, invisible to it (objectui#9519). Widening that gate's
* population is a scope question of its own and is NOT decided here; this test
* holds the four known ends of one path together, by content.
*
* ## What "the same tree" cost when it was not held
*
* Measured for objectui#9519 on this tree: a live run launched from `e2e/`
* really created `e2e/e2e/live/.auth/state.json`, while the repaired read went
* looking under the repository root and found nothing. Playwright itself does
* not save anyone here — `use.storageState` is resolved against the PROCESS
* CWD, unlike `testDir` and `globalSetup`, which are config-dir rooted.
*/
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

const SELF_DEPTH_BELOW_REPO_ROOT = 3; // scripts / __tests__ / this file
const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname)
.split('/')
.slice(0, -SELF_DEPTH_BELOW_REPO_ROOT)
.join('/');

/** Every file that names the live storage state, and how it must root it. */
const PARTICIPANTS = [
{ file: 'e2e/live/global-setup.ts', side: 'writes it' },
{ file: 'playwright.live.config.ts', side: 'names it to Playwright' },
{ file: 'playwright.import-console.config.ts', side: 'names it to Playwright' },
{ file: 'e2e/live/inline-edit-polish-2572.spec.ts', side: 'reads it' },
] as const;

/** A bare relative path reaching a filesystem call or the `storageState` option. */
const AMBIENT_ROOTED =
/(storageState\s*:|readFileSync\(|writeFileSync\(|mkdirSync\(|existsSync\()\s*'e2e\/live\/\.auth/;

describe('live-e2e storage state — one path, one root (objectui#9519)', () => {
for (const { file, side } of PARTICIPANTS) {
it(`${file} roots the state path on its own file (${side})`, () => {
const src = readFileSync(join(REPO_ROOT, file), 'utf8');

expect(src).toContain('SELF_DEPTH_BELOW_REPO_ROOT');
expect(src).toContain('new URL(import.meta.url).pathname');
expect(src).toMatch(/join\(\s*REPO_ROOT,\s*'e2e\/live\/\.auth\/state\.json'\s*\)/);
expect(src).not.toMatch(AMBIENT_ROOTED);
});
}

it('the detector fires on the spelling this card repaired', () => {
expect(" storageState: 'e2e/live/.auth/state.json',").toMatch(AMBIENT_ROOTED);
expect("const STATE_PATH = join(REPO_ROOT, 'e2e/live/.auth/state.json');").not.toMatch(
AMBIENT_ROOTED,
);
});
});
Loading