mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 05:36:17 -08:00
fix(e2e): read sidebar categories atomically; tighten mock launch guard
- category-management: readVisibleSidebarCategoryNames read items one by
one; when Save removed an item between isVisible() and textContent(),
textContent() auto-waited for the gone label through the whole 15 s
poll, so expect.poll never retried (ubuntu shard 1 failed 3/3 while the
UI already showed "No categories available"). Take one snapshot with
filter({ visible: true }).evaluateAll() instead.
- project-config.spec: pin which Playwright configs start which mock,
reject any Nx form that mentions a mock server, and fail when a new
config starts a mock without being listed (the old count check passed
vacuously on zero matches).
- docs: state which configs start which mock instead of "every config
starts both".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ad83d4781
commit
8848b088e9
3 files changed
+70
-59
No files matched your search
@@ -591,29 +591,22 @@ async function expectVisibleSidebarCategoryNames(
|
||||
}
|
||||
}
|
||||
|
||||
// One DOM snapshot: reading items one by one raced list updates, and
|
||||
// `textContent()` on a removed item auto-waited through the whole poll.
|
||||
async function readVisibleSidebarCategoryNames(page: Page): Promise<string[]> {
|
||||
const categories = page.locator(
|
||||
'app-workspace-context-panel .category-item'
|
||||
);
|
||||
const actualNames: string[] = [];
|
||||
const count = await categories.count();
|
||||
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const category = categories.nth(index);
|
||||
if (!(await category.isVisible())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const categoryName =
|
||||
(await category.locator('.nav-item-label').textContent())?.trim() ??
|
||||
'';
|
||||
|
||||
if (categoryName) {
|
||||
actualNames.push(categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
return actualNames;
|
||||
return page
|
||||
.locator('app-workspace-context-panel .category-item')
|
||||
.filter({ visible: true })
|
||||
.evaluateAll((categories) =>
|
||||
categories
|
||||
.map(
|
||||
(category) =>
|
||||
category
|
||||
.querySelector('.nav-item-label')
|
||||
?.textContent?.trim() ?? ''
|
||||
)
|
||||
.filter((name) => name.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
function stringArraysEqual(left: string[], right: string[]): boolean {
|
||||
|
||||
@@ -29,46 +29,58 @@ describe('Xtream mock Nx serve environment', () => {
|
||||
|
||||
describe('Playwright mock-server launch', () => {
|
||||
const appsDirectory = join(process.cwd(), 'apps');
|
||||
const playwrightConfigs = readdirSync(appsDirectory, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.flatMap((entry) =>
|
||||
readdirSync(join(appsDirectory, entry.name))
|
||||
.filter((name) => /^playwright(\..+)?\.config\.ts$/.test(name))
|
||||
.map((name) => `apps/${entry.name}/${name}`)
|
||||
);
|
||||
const expectedMockLaunches: Record<string, string[]> = {
|
||||
'apps/electron-backend-e2e/playwright.config.ts': ['stalker', 'xtream'],
|
||||
'apps/electron-backend-e2e/playwright.journeys.config.ts': ['xtream'],
|
||||
'apps/electron-backend-e2e/playwright.xtream-performance.config.ts': [
|
||||
'xtream',
|
||||
],
|
||||
'apps/web-e2e/playwright.config.ts': ['stalker', 'xtream'],
|
||||
};
|
||||
const readSource = (configPath: string) =>
|
||||
readFileSync(join(process.cwd(), configPath), 'utf8');
|
||||
|
||||
it('finds every config that starts a mock server', () => {
|
||||
expect(playwrightConfigs).toEqual(
|
||||
expect.arrayContaining([
|
||||
'apps/electron-backend-e2e/playwright.config.ts',
|
||||
'apps/electron-backend-e2e/playwright.journeys.config.ts',
|
||||
'apps/electron-backend-e2e/playwright.xtream-performance.config.ts',
|
||||
'apps/web-e2e/playwright.config.ts',
|
||||
])
|
||||
it('knows every Playwright config that starts a mock server', () => {
|
||||
const configsUsingMocks = readdirSync(appsDirectory, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.flatMap((entry) =>
|
||||
readdirSync(join(appsDirectory, entry.name))
|
||||
.filter((name) =>
|
||||
/^playwright(\..+)?\.config\.ts$/.test(name)
|
||||
)
|
||||
.map((name) => `apps/${entry.name}/${name}`)
|
||||
)
|
||||
.filter((configPath) =>
|
||||
readSource(configPath).includes('-mock-server')
|
||||
);
|
||||
|
||||
expect(configsUsingMocks.sort()).toEqual(
|
||||
Object.keys(expectedMockLaunches).sort()
|
||||
);
|
||||
});
|
||||
|
||||
// Nx starts a run-commands child in its own detached process group, and
|
||||
// Playwright stops a webServer with a process-group SIGKILL, so a mock
|
||||
// launched through `nx run …:serve` outlived the run and kept its port.
|
||||
it.each(playwrightConfigs)(
|
||||
'starts mock servers as a single node process in %s',
|
||||
(configPath) => {
|
||||
const source = readFileSync(
|
||||
join(process.cwd(), configPath),
|
||||
'utf8'
|
||||
);
|
||||
// launched through Nx outlived the run and kept its port.
|
||||
it.each(Object.entries(expectedMockLaunches))(
|
||||
'starts each mock in %s as a single node process',
|
||||
(configPath, mocks) => {
|
||||
const source = readSource(configPath);
|
||||
|
||||
expect(source).not.toMatch(/nx run [\w-]+-mock-server:serve/);
|
||||
const launches =
|
||||
source.match(/node --import tsx apps\/[\w-]+-mock-server\//g) ??
|
||||
[];
|
||||
const tsconfigs =
|
||||
source.match(/TSX_TSCONFIG_PATH: 'tsconfig\.base\.json'/g) ??
|
||||
[];
|
||||
expect(tsconfigs).toHaveLength(launches.length);
|
||||
expect(source).not.toMatch(/\bnx\b[^'"`\n]*mock-server/);
|
||||
for (const mock of mocks) {
|
||||
expect(source).toContain(
|
||||
`'node --import tsx apps/${mock}-mock-server/src/main.ts'`
|
||||
);
|
||||
}
|
||||
expect(
|
||||
source.match(/node --import tsx apps\/[\w-]+-mock-server\//g)
|
||||
).toHaveLength(mocks.length);
|
||||
expect(
|
||||
source.match(/TSX_TSCONFIG_PATH: 'tsconfig\.base\.json'/g)
|
||||
).toHaveLength(mocks.length);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -561,8 +561,13 @@ The mock server is listed as a third `webServer` entry:
|
||||
}
|
||||
```
|
||||
|
||||
Every Playwright config (web, Electron, Xtream benchmark, journeys) starts the
|
||||
Xtream and Stalker mocks this way, never through `pnpm nx run …:serve`.
|
||||
Every mock-server `webServer` entry uses this launch form, never
|
||||
`pnpm nx run …:serve`. The web and Electron E2E configs
|
||||
(`apps/web-e2e/playwright.config.ts`,
|
||||
`apps/electron-backend-e2e/playwright.config.ts`) start both the Stalker and
|
||||
the Xtream mock. The journeys and Xtream benchmark configs start only the
|
||||
Xtream mock, on their own loopback ports. The packaged and other performance
|
||||
configs start no mock.
|
||||
Playwright stops a `webServer` by sending `SIGKILL` to the process group it
|
||||
spawned (`taskkill /T /F` on Windows), and Nx `run-commands` starts its command
|
||||
in a detached process group of its own. The kill therefore reached only
|
||||
@@ -571,8 +576,9 @@ the next run fail with "…/health is already used" or, with
|
||||
`reuseExistingServer`, silently reuse a stale server. `node --import tsx` keeps
|
||||
the server a single process in Playwright's group. `TSX_TSCONFIG_PATH` stands in
|
||||
for the serve target's `--tsconfig` flag and is required for the `@iptvnator/*`
|
||||
path aliases. `project-config.spec.ts` guards every `playwright*.config.ts`
|
||||
against regressing to the Nx launch. The `serve` targets remain the entry point
|
||||
path aliases. `project-config.spec.ts` pins which configs start which mock,
|
||||
and fails if any of them launches a mock through Nx or a new config starts one
|
||||
without being listed there. The `serve` targets remain the entry point
|
||||
for starting a mock by hand.
|
||||
|
||||
### Request Interception
|
||||
|
||||
Reference in new issue
Block a user