This commit is contained in:
@@ -2,7 +2,8 @@ import { test, expect, type Page } from '@playwright/test';
|
||||
import { createRequire } from 'node:module';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { openSpaceTab } from '../e2e/helpers/spaceTabs';
|
||||
import { logout } from './helpers/session';
|
||||
|
||||
// ── Local-auth shared-space E2E ───────────────────────────────────────────────
|
||||
//
|
||||
@@ -28,7 +29,9 @@ const repoRoot = resolve(__dirname, '..', '..');
|
||||
// independently — rather than importing the config — keeps the spec decoupled
|
||||
// from Playwright's config-resolution internals while guaranteeing both open
|
||||
// the SAME SQLite file.
|
||||
const DB_PATH = join(tmpdir(), 'maestro-e2e-auth', 'e2e-auth.db');
|
||||
const e2eTmp = process.env.E2E_AUTH_TMP;
|
||||
if (!e2eTmp) throw new Error('E2E_AUTH_TMP must be set by playwright.auth.config.ts');
|
||||
const DB_PATH = join(e2eTmp, 'e2e-auth.db');
|
||||
|
||||
const ADMIN = { email: '[email protected]', password: 'AdminPass123!' };
|
||||
const ALICE = { email: '[email protected]', password: 'AlicePass123!', name: 'Alice' };
|
||||
@@ -55,12 +58,6 @@ const BENIGN_PATHS = new Set([
|
||||
const BENIGN_ASSET = /\.(ico|png|svg|map|webmanifest|json)$/i;
|
||||
const BENIGN_PATH_RE =
|
||||
/\/api\/local\/tasks\/\d+\/console\/status$|\/api\/local\/browser\/sessions\/task-session\/\d+$/;
|
||||
// The space members GET is auth-gated. During the logout→login handoff (test 3),
|
||||
// the header avatar query can fire ONE request after the cookie is cleared but
|
||||
// before the new session lands → a transient 401. Only a 401 (unauthenticated)
|
||||
// is benign here; a 403 (authenticated-but-forbidden) would be a real authz bug
|
||||
// and is NOT excused. The avatar assertion still proves the eventual 200.
|
||||
const SPACE_MEMBERS_RE = /\/api\/local\/spaces\/[^/]+\/members$/;
|
||||
|
||||
function trackFatalErrors(page: Page): string[] {
|
||||
const fatalErrors: string[] = [];
|
||||
@@ -68,12 +65,10 @@ function trackFatalErrors(page: Page): string[] {
|
||||
page.on('response', (res) => {
|
||||
if (res.status() >= 400) {
|
||||
const { pathname } = new URL(res.url());
|
||||
const transientMembers401 = res.status() === 401 && SPACE_MEMBERS_RE.test(pathname);
|
||||
if (
|
||||
!BENIGN_ASSET.test(pathname) &&
|
||||
!BENIGN_PATHS.has(pathname) &&
|
||||
!BENIGN_PATH_RE.test(pathname) &&
|
||||
!transientMembers401
|
||||
!BENIGN_PATH_RE.test(pathname)
|
||||
) {
|
||||
fatalErrors.push(`HTTP ${res.status()} ${res.url()}`);
|
||||
}
|
||||
@@ -97,10 +92,6 @@ async function login(page: Page, email: string, password: string) {
|
||||
expect(page.url(), 'login should not bounce back to /auth/login').not.toContain('/auth/login');
|
||||
}
|
||||
|
||||
async function logout(page: Page) {
|
||||
await page.goto('/auth/logout');
|
||||
}
|
||||
|
||||
/** Open Spaces, create a case space with a unique title, select it, and return
|
||||
* the detail locator + the resolved space id. Mirrors the auth-off helper. */
|
||||
async function createAndOpenCaseSpace(page: Page, label: string) {
|
||||
@@ -132,7 +123,7 @@ async function createAndOpenCaseSpace(page: Page, label: string) {
|
||||
|
||||
/** Navigate the already-open space to 設定 → メンバー and return the panel. */
|
||||
async function openMembersPanel(page: Page) {
|
||||
await page.getByTestId('space-tab-settings').click();
|
||||
await openSpaceTab(page, 'settings');
|
||||
await page.getByTestId('space-settings-nav-members').click();
|
||||
const panel = page.getByTestId('space-members-panel');
|
||||
await expect(panel).toBeVisible({ timeout: 15_000 });
|
||||
@@ -232,7 +223,11 @@ test('invite alice → member row + header avatars appear', async ({ page }) =>
|
||||
await inviteMember(page, ALICE.name);
|
||||
|
||||
// Alice now appears as a member row in the panel.
|
||||
const aliceRow = panel.locator('[data-testid^="space-member-"]').filter({ hasText: ALICE.name });
|
||||
// 招待の一覧(space-member-picker)は除く。候補として同じ名前を出すので、
|
||||
// 含めたままだと「行が出た」と言いながら一覧を見ているだけになりうる。
|
||||
const aliceRow = panel
|
||||
.locator('[data-testid^="space-member-"]:not([data-testid^="space-member-picker"])')
|
||||
.filter({ hasText: ALICE.name });
|
||||
await expect(aliceRow.first()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// AND the header avatar stack renders now that the space has >= 2 members.
|
||||
@@ -259,7 +254,11 @@ test('alice sees + opens the shared space, avatars render', async ({ page }) =>
|
||||
const { spaceId } = await createAndOpenCaseSpace(page, 'E2E協働');
|
||||
const panel = await openMembersPanel(page);
|
||||
await inviteMember(page, ALICE.name);
|
||||
const aliceRow = panel.locator('[data-testid^="space-member-"]').filter({ hasText: ALICE.name });
|
||||
// 招待の一覧(space-member-picker)は除く。候補として同じ名前を出すので、
|
||||
// 含めたままだと「行が出た」と言いながら一覧を見ているだけになりうる。
|
||||
const aliceRow = panel
|
||||
.locator('[data-testid^="space-member-"]:not([data-testid^="space-member-picker"])')
|
||||
.filter({ hasText: ALICE.name });
|
||||
await expect(aliceRow.first()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Switch to alice.
|
||||
@@ -295,10 +294,16 @@ test('admin changes alice role then removes her', async ({ page }) => {
|
||||
await inviteMember(page, ALICE.name);
|
||||
|
||||
// Resolve alice's member row + her userId from the row testid suffix
|
||||
// (`space-member-{userId}`; the row wrapper is the first prefix match).
|
||||
// (`space-member-{userId}`).
|
||||
//
|
||||
// 招待の一覧(space-member-picker)を必ず外すこと。あれも
|
||||
// `space-member-` で始まる div で、中に候補として同じ名前が出る。追加した
|
||||
// 直後は行がまだ描かれていないのに一覧だけ残っている瞬間があり、そこを掴むと
|
||||
// userId が 'picker' になって、この後の space-member-role-picker が永遠に
|
||||
// 見つからない(#1399。実際にこの並びで落ちた)。
|
||||
const aliceRow = () =>
|
||||
page.getByTestId('space-members-panel')
|
||||
.locator('div[data-testid^="space-member-"]')
|
||||
.locator('div[data-testid^="space-member-"]:not([data-testid^="space-member-picker"])')
|
||||
.filter({ hasText: ALICE.name })
|
||||
.first();
|
||||
await expect(aliceRow()).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
Reference in New Issue
Block a user