# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: 60-invitations.spec.ts >> invitations: revoked offer fails closed for the invited existing account
- Location: e2e-scheduling-real/60-invitations.spec.ts:164:5

# Error details

```
Error: expect(locator).toContainText(expected) failed

Locator: getByRole('alert')
Expected substring: "Could not accept this invitation."
Error: strict mode violation: getByRole('alert') resolved to 2 elements:
    1) <p role="alert" class="mt-4 text-sm text-red-700">Could not accept this invitation. It may be expir…</p> aka getByText('Could not accept this')
    2) <div role="alert" aria-live="assertive" id="__next-route-announcer__">Store invitation | Alqove</div> aka getByText('Store invitation | Alqove')

Call log:
  - Expect "toContainText" with timeout 20000ms
  - waiting for getByRole('alert')

```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - generic [ref=e3]:
    - link "Alqove" [ref=e4] [cursor=pointer]:
      - /url: /
      - img "Alqove" [ref=e5]
    - generic [ref=e7]:
      - heading "Store invitation" [level=1] [ref=e8]
      - paragraph [ref=e9]: Signed in as e2e-0d4345c5b94c18e8.invite-rejected@example.test. Accept only if this is the account invited to your store.
      - alert [ref=e10]: Could not accept this invitation. It may be expired, revoked, already used, or intended for a different account. Check your account or ask your manager for help, then try again.
      - button "Accept invitation" [ref=e11]
      - button "Use a different account" [ref=e12]
      - button "Cancel invitation" [ref=e13]
  - button "Open Next.js Dev Tools" [ref=e19] [cursor=pointer]:
    - img [ref=e20]
  - alert [ref=e23]: Store invitation | Alqove
```

# Test source

```ts
  79  |   await expect(page.getByText(`Signed in as ${actor.email}.`, { exact: false })).toBeVisible();
  80  |   return body.data as { id: string; store_id: string; role: string; status: string }[];
  81  | }
  82  | 
  83  | function assertUnaccepted(state: Readback, invite: Invite, actor: InviteActor) {
  84  |   const row = state.invitations.find(row => row.id === invite.id);
  85  |   expect(row).toMatchObject({ store_id: invite.store_id, invited_user_id: invite.invited_user_id, accepted_at: null });
  86  |   expect(state.memberships.filter(row => row.user_id === actor.id && row.store_id === invite.store_id)).toEqual([]);
  87  | }
  88  | 
  89  | test.beforeAll(() => {
  90  |   fixtureCommand('seed');
  91  |   invites = JSON.parse(fs.readFileSync(path.join(runDir, 'manifest.json'), 'utf8')).inviteFixtures;
  92  |   if (!invites) throw new Error('Invitation fixture manifest missing');
  93  | });
  94  | 
  95  | test('invitations: existing account continues login, explicitly accepts, discovers and selects staff store', async ({ page }) => {
  96  |   const invite = invites.invitations.accepted;
  97  |   const actor = invites.actors.recipient;
  98  |   let acceptCount = 0;
  99  |   page.on('request', request => {
  100 |     if (new URL(request.url()).pathname === '/v1/invitations/accept' && request.method() === 'POST') acceptCount++;
  101 |   });
  102 |   assertUnaccepted(readback(), invite, actor);
  103 |   await landing(page, invite);
  104 |   const before = await continueLogin(page, actor);
  105 |   expect(before.map(row => row.store_id)).toEqual([fixture.stores.a.id]);
  106 |   expect(acceptCount, 'Login must not implicitly accept an invitation').toBe(0);
  107 |   assertUnaccepted(readback(), invite, actor);
  108 | 
  109 |   // Register both real response observers before the explicit UI command.
  110 |   const accepted = page.waitForResponse(isAccept);
  111 |   const discovered = page.waitForResponse(isDiscovery);
  112 |   await page.getByRole('button', { name: 'Accept invitation', exact: true }).click();
  113 |   const acceptance = await accepted;
  114 |   expect(acceptance.status()).toBe(201);
  115 |   const membership = (await acceptance.json()).data;
  116 |   expect(membership).toMatchObject({ store_id: invite.store_id, role: 'sales', status: 'active' });
  117 |   const discovery = await discovered;
  118 |   expect(discovery.status()).toBe(200);
  119 |   const memberships = (await discovery.json()).data as { id: string; store_id: string }[];
  120 |   expect(memberships.map(row => row.store_id).sort()).toEqual([fixture.stores.a.id, fixture.stores.b.id].sort());
  121 |   expect(memberships.find(row => row.id === membership.id)?.store_id).toBe(invite.store_id);
  122 |   await page.waitForURL(url => url.pathname === '/staff');
  123 |   await expect(page.getByRole('heading', { name: 'Your staff workspace', exact: true })).toBeVisible();
  124 |   const store = page.getByRole('combobox', { name: 'Store', exact: true });
  125 |   await expect(store).toHaveValue(invite.store_id);
  126 |   expect(await page.evaluate(id => localStorage.getItem(`selected_store:${id}`), actor.id)).toBe(invite.store_id);
  127 |   expect(await page.evaluate(() => sessionStorage.getItem('alqove.pending-invitation') === null)).toBe(true);
  128 |   const state = readback();
  129 |   const row = state.invitations.find(row => row.id === invite.id)!;
  130 |   expect(row.accepted_at).not.toBeNull();
  131 |   expect(row.revoked_at).toBeNull();
  132 |   const exactMembers = state.memberships.filter(row => row.user_id === actor.id && row.store_id === invite.store_id);
  133 |   expect(exactMembers).toEqual([{ id: membership.id, user_id: actor.id, store_id: invite.store_id, role: 'sales', status: 'active' }]);
  134 |   await page.reload();
  135 |   await expect(page.getByRole('heading', { name: 'Your staff workspace', exact: true })).toBeVisible();
  136 |   await expect(store).toHaveValue(invite.store_id);
  137 |   expect(acceptCount).toBe(1);
  138 |   await evidence('invitations-accepted', { invitation: row, membership: exactMembers[0], discovered_store_ids: memberships.map(row => row.store_id), selected_store_id: invite.store_id, accept_request_count: acceptCount, persisted_after_reload: true, token_cleared: true, token_delivery: 'pre-hydration browser URL; no token-bearing HTTP GET' });
  139 | });
  140 | 
  141 | test('invitations: wrong account fails closed without membership or acceptance', async ({ page }) => {
  142 |   const invite = invites.invitations.wrong;
  143 |   const actor = fixture.actors.outsider as InviteActor;
  144 |   await landing(page, invite);
  145 |   expect(await continueLogin(page, actor)).toEqual([]);
  146 |   const accepted = page.waitForResponse(isAccept);
  147 |   await page.getByRole('button', { name: 'Accept invitation', exact: true }).click();
  148 |   const response = await accepted;
  149 |   expect(response.status()).toBe(422);
  150 |   expect((await response.json()).errors?.token).toContain('This invitation is for a different account.');
  151 |   await expect(page.getByRole('alert')).toContainText('Could not accept this invitation.');
  152 |   await expect(page.getByRole('button', { name: 'Accept invitation', exact: true })).toBeEnabled();
  153 |   expect(new URL(page.url()).pathname).toBe('/invitations/accept');
  154 |   const state = readback();
  155 |   assertUnaccepted(state, invite, actor);
  156 |   assertUnaccepted(state, invite, invites.actors.rejected);
  157 |   expect(await page.evaluate(id => localStorage.getItem(`selected_store:${id}`), actor.id)).toBeNull();
  158 |   await page.goto('/staff');
  159 |   await expect(page.getByText('No active store memberships. Ask your store manager for an invitation.', { exact: true })).toBeVisible();
  160 |   await expect(page.getByRole('navigation', { name: 'Staff', exact: true })).toHaveCount(0);
  161 |   await evidence('invitations-wrong-account', { invitation_id: invite.id, wrong_account_id: actor.id, status: response.status(), database: state, staff_access_denied: true });
  162 | });
  163 | 
  164 | test('invitations: revoked offer fails closed for the invited existing account', async ({ page, request }) => {
  165 |   const invite = invites.invitations.revoked;
  166 |   const actor = invites.actors.rejected;
  167 |   // Revoke via the real API, not a fixture status mutation.
  168 |   await api(request, 'owner', 'DELETE', `/invitations/${invite.id}`, undefined, 204, 'b');
  169 |   const revoked = readback().invitations.find(row => row.id === invite.id)!;
  170 |   expect(revoked.revoked_at).not.toBeNull();
  171 |   expect(revoked.accepted_at).toBeNull();
  172 |   await landing(page, invite);
  173 |   expect(await continueLogin(page, actor)).toEqual([]);
  174 |   const accepted = page.waitForResponse(isAccept);
  175 |   await page.getByRole('button', { name: 'Accept invitation', exact: true }).click();
  176 |   const response = await accepted;
  177 |   expect(response.status()).toBe(422);
  178 |   expect((await response.json()).errors?.token).toContain('This invitation is invalid or has expired.');
> 179 |   await expect(page.getByRole('alert')).toContainText('Could not accept this invitation.');
      |                                         ^ Error: expect(locator).toContainText(expected) failed
  180 |   expect(new URL(page.url()).pathname).toBe('/invitations/accept');
  181 |   const state = readback();
  182 |   assertUnaccepted(state, invite, actor);
  183 |   expect(state.invitations.find(row => row.id === invite.id)?.revoked_at).toBe(revoked.revoked_at);
  184 |   expect(await page.evaluate(id => localStorage.getItem(`selected_store:${id}`), actor.id)).toBeNull();
  185 |   await page.goto('/staff');
  186 |   await expect(page.getByText('No active store memberships. Ask your store manager for an invitation.', { exact: true })).toBeVisible();
  187 |   await expect(page.getByRole('navigation', { name: 'Staff', exact: true })).toHaveCount(0);
  188 |   await evidence('invitations-revoked', { invitation_id: invite.id, status: response.status(), database: state, staff_access_denied: true });
  189 | });
  190 | 
```