Files
LivingDexTracker/tests/bdd/steps/pokedex-lifecycle.steps.ts
T
2026-09-14 12:56:09 +01:00

171 lines
6.4 KiB
TypeScript

import { createBdd } from 'playwright-bdd';
import { test, expect } from '../fixtures';
import { createDexThroughUi, deleteAllPokedexes } from '../support/app';
import { requireLoopbackUrl } from '../../support/loopback';
const { Given, When, Then } = createBdd(test);
async function dexCard(page: Parameters<typeof createDexThroughUi>[0], name: string) {
await page.goto('/my-pokedexes');
const card = page.locator('.card').filter({ hasText: name }).first();
await expect(card).toBeVisible();
return card;
}
// Establishes the precondition rather than asserting it - a fresh user happens to be empty,
// which would make an assertion here pass without testing anything.
Given('I have no Pokédexes', async ({ state }) => {
await deleteAllPokedexes(state);
});
Given('I have a Living Dex named {string}', async ({ page, state }, name: string) => {
await createDexThroughUi(page, state, { name, type: 'Living Dex' });
});
Given('I have a Form Dex named {string}', async ({ page, state }, name: string) => {
await createDexThroughUi(page, state, { name, type: 'Form Dex' });
});
Given(
'I have a Form Dex named {string} scoped to game {string} and dex {string}',
async ({ page, state }, name: string, game: string, dex: string) => {
await createDexThroughUi(page, state, { name, type: 'Form Dex', game, dex });
}
);
Given('I have a Shiny Dex named {string}', async ({ page, state }, name: string) => {
await createDexThroughUi(page, state, { name, type: 'Shiny Dex' });
});
Given('another trainer has a Pokédex', async ({ state }) => {
const url = requireLoopbackUrl(
process.env.TEST_SUPABASE_URL ?? 'http://127.0.0.1:54321',
'TEST_SUPABASE_URL'
);
const key = process.env.E2E_SERVICE_ROLE_KEY;
if (!key) throw new Error('E2E_SERVICE_ROLE_KEY is required');
const headers = {
apikey: key,
Authorization: `Bearer ${key}`,
'Content-Type': 'application/json'
};
const userResponse = await fetch(`${url}/auth/v1/admin/users`, {
method: 'POST',
headers,
body: JSON.stringify({
email: `other-${Date.now()}@example.test`,
password: 'OtherPassword123!',
email_confirm: true
})
});
const other = (await userResponse.json()) as { id: string };
const dexResponse = await fetch(`${url}/rest/v1/pokedexes`, {
method: 'POST',
headers: { ...headers, Prefer: 'return=representation' },
body: JSON.stringify({ userId: other.id, name: 'Private', isLivingDex: true })
});
if (!dexResponse.ok) throw new Error(await dexResponse.text());
const [dex] = (await dexResponse.json()) as Array<{ id: string }>;
state.pokedexId = dex.id;
});
When('I visit my Pokédex list', async ({ page }) => {
await page.goto('/my-pokedexes');
});
When('I open the new Pokédex form', async ({ page }) => {
await page.goto('/my-pokedexes');
await page
.getByRole('button', { name: /Create (New|Your First) Pokédex/ })
.first()
.click();
});
When('I create a Pokédex named {string} of type {string}', async ({ page, state }, name, type) => {
await createDexThroughUi(page, state, { name, type });
});
When(
'I create a Living Dex named {string} scoped to game {string} and dex {string}',
async ({ page, state }, name: string, game: string, dex: string) => {
await createDexThroughUi(page, state, { name, type: 'Living Dex', game, dex });
}
);
When('I try to create another Living Dex named {string}', async ({ page, state }, name: string) => {
await page.goto('/my-pokedexes');
await page.getByRole('button', { name: 'Create New Pokédex', exact: true }).click();
const modal = page.locator('.modal-open');
await modal.getByLabel('Name').fill(name);
await modal.getByText('Living Dex', { exact: false }).locator('..').getByRole('checkbox').check();
page.once('dialog', async (dialog) => {
state.lastMessage = dialog.message();
await dialog.accept();
});
await modal.getByRole('button', { name: 'Create', exact: true }).click();
await expect.poll(() => state.lastMessage).not.toBeNull();
});
When('I rename it to {string} and enable forms', async ({ page }, name: string) => {
const card = await dexCard(page, 'Before Editing');
await card.getByRole('button', { name: 'Edit' }).click();
const modal = page.locator('.modal-open');
await modal.getByLabel('Name').fill(name);
await modal.getByText('Form Dex', { exact: false }).locator('..').getByRole('checkbox').check();
const response = page.waitForResponse(
(candidate) =>
candidate.url().includes('/api/pokedexes/') && candidate.request().method() === 'PUT'
);
await modal.getByRole('button', { name: 'Save', exact: true }).click();
expect((await response).ok()).toBe(true);
await expect(modal).toHaveCount(0);
});
When('I cancel deleting {string}', async ({ page }, name: string) => {
const card = await dexCard(page, name);
page.once('dialog', (dialog) => dialog.dismiss());
await card.getByRole('button', { name: 'Delete' }).click();
});
When('I confirm deleting {string}', async ({ page }, name: string) => {
const card = await dexCard(page, name);
page.once('dialog', (dialog) => dialog.accept());
const response = page.waitForResponse(
(candidate) =>
candidate.url().includes('/api/pokedexes/') && candidate.request().method() === 'DELETE'
);
await card.getByRole('button', { name: 'Delete' }).click();
expect((await response).ok()).toBe(true);
});
When("I request the other trainer's Pokédex", async ({ page, state }) => {
const response = await page.request.get(`/api/pokedexes/${state.pokedexId}`);
state.lastResponseStatus = response.status();
});
Then('I see the empty Pokédex message', async ({ page }) => {
await expect(page.getByText("You haven't created any pokédexes yet!")).toBeVisible();
});
Then('I cannot create a Pokédex without a name and type', async ({ page }) => {
await expect(page.getByRole('button', { name: 'Create', exact: true })).toBeDisabled();
await expect(page.getByText('At least one type required')).toBeVisible();
});
Then('the Pokédex {string} is available to view', async ({ page }, name: string) => {
await expect((await dexCard(page, name)).getByRole('button', { name: 'View' })).toBeVisible();
});
Then('I am told that the Pokédex name is already used', async ({ state }) => {
expect(state.lastMessage).toMatch(/already have a Pokédex named/i);
});
Then('the Pokédex {string} is no longer listed', async ({ page }, name: string) => {
await page.goto('/my-pokedexes');
await expect(page.locator('.card').filter({ hasText: name })).toHaveCount(0);
});
Then('the Pokédex is not disclosed', async ({ state }) => {
expect(state.lastResponseStatus).toBe(404);
});