Skip to content

Commit 19af67d

Browse files
committed
Linter
1 parent d6f463c commit 19af67d

10 files changed

Lines changed: 62 additions & 32 deletions

playwright.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ export default defineConfig({
2727
fullyParallel: true,
2828
forbidOnly: !!process.env.CI,
2929
retries: process.env.CI ? 2 : 0,
30-
reporter: process.env.CI ? 'github' : 'list',
30+
// In CI: 'github' annotates the Actions log, and 'html' writes the
31+
// playwright-report/ dir that the CI workflow uploads as an artifact.
32+
reporter: process.env.CI
33+
? [['github'], ['html', { open: 'never' }]]
34+
: 'list',
3135
// The Nuxt dev server compiles routes lazily on first hit, so a cold parallel
3236
// request can take several seconds to render. A roomy expect timeout absorbs
3337
// that without masking real failures. (Run E2E against `nuxt build && preview`

services/export/tdei.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ export class TdeiExporter {
103103
}
104104

105105
async _exportOswToTdei(workspace: Workspace, metadata: any): Promise<string> {
106+
const tdeiServiceId = this._requireServiceId(workspace);
107+
106108
this._context.status = status.exportOsm;
107109
const osm = await this._osmClient.exportWorkspaceXml(workspace.id);
108110

@@ -113,7 +115,7 @@ export class TdeiExporter {
113115
const jobId = await this._tdeiClient.uploadOswDataset(
114116
(await this._filterNonexistentDataset(workspace.tdeiRecordId)),
115117
workspace.tdeiProjectGroupId,
116-
workspace.tdeiServiceId!,
118+
tdeiServiceId,
117119
oswZip,
118120
{ dataset_detail: metadata }
119121
);
@@ -124,6 +126,8 @@ export class TdeiExporter {
124126
}
125127

126128
async _exportPathwaysToTdei(workspace: Workspace, metadata: any): Promise<string> {
129+
const tdeiServiceId = this._requireServiceId(workspace);
130+
127131
this._context.status = status.exportOsm;
128132
const elements = await this._osmClient.getWorkspaceData(workspace.id);
129133
const derivedFromDatasetId = await this._filterNonexistentDataset(workspace.tdeiRecordId);
@@ -136,7 +140,7 @@ export class TdeiExporter {
136140
const jobId = await this._tdeiClient.uploadPathwaysDataset(
137141
derivedFromDatasetId,
138142
workspace.tdeiProjectGroupId,
139-
workspace.tdeiServiceId!,
143+
tdeiServiceId,
140144
csvZip,
141145
{ dataset_detail: metadata }
142146
);
@@ -146,6 +150,19 @@ export class TdeiExporter {
146150
return jobId
147151
}
148152

153+
// The TDEI upload endpoints require a service id to build the request URL.
154+
// Validate it up front so we fail fast with a clear message instead of
155+
// sending `undefined` onward to the service layer.
156+
_requireServiceId(workspace: Workspace): string {
157+
if (!workspace.tdeiServiceId) {
158+
throw new Error(
159+
`Workspace ${workspace.id} has no TDEI service id; cannot export to TDEI.`
160+
);
161+
}
162+
163+
return workspace.tdeiServiceId;
164+
}
165+
149166
async _filterNonexistentDataset(tdeiRecordId: string | undefined): Promise<string | undefined> {
150167
if (!tdeiRecordId) {
151168
return undefined;

test/e2e/contract.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,27 @@ export function recordContract(page: Page) {
134134
// Response body
135135
const resDef = op.responses?.[String(c.status)] ?? op.responses?.default;
136136
const resSchema = resDef?.content?.['application/json']?.schema;
137-
if (resSchema && c.contentType.includes('application/json') && c.resText) {
138-
try {
139-
const v = validatorFor(resSchema, `${match.key}|${c.method}|res|${c.status}`);
140-
if (!v(JSON.parse(c.resText))) {
137+
// Validate any response that claims application/json, even with an empty
138+
// body: an empty/missing body where the spec expects JSON is a violation,
139+
// not something to skip (gating on c.resText hid that case).
140+
if (resSchema && c.contentType.includes('application/json')) {
141+
if (!c.resText) {
142+
out.push({ method: c.method, path: c.recordedPath, status: c.status, where: 'response',
143+
message: 'expected an application/json response body but it was empty' });
144+
}
145+
else {
146+
try {
147+
const v = validatorFor(resSchema, `${match.key}|${c.method}|res|${c.status}`);
148+
if (!v(JSON.parse(c.resText))) {
149+
out.push({ method: c.method, path: c.recordedPath, status: c.status, where: 'response',
150+
message: ajv.errorsText(v.errors) });
151+
}
152+
}
153+
catch (e) {
141154
out.push({ method: c.method, path: c.recordedPath, status: c.status, where: 'response',
142-
message: ajv.errorsText(v.errors) });
155+
message: `response body not valid JSON: ${(e as Error).message}` });
143156
}
144157
}
145-
catch (e) {
146-
out.push({ method: c.method, path: c.recordedPath, status: c.status, where: 'response',
147-
message: `response body not valid JSON: ${(e as Error).message}` });
148-
}
149158
}
150159
}
151160
return out;

test/e2e/dashboard.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test, expect, seedAuthenticatedSession, seedProjectGroupSelection } from './fixtures';
22
import { recordContract } from './contract';
3-
import { projectGroups, PROJECT_GROUP_ID } from '../mocks/fixtures';
3+
import { projectGroups, PROJECT_GROUP_ID, TEST_API_BASE } from '../mocks/fixtures';
44

55
// Generated from the @test outline in pages/dashboard.vue.
66
//
@@ -21,8 +21,8 @@ test.describe('dashboard', () => {
2121
// (The populated WorkspaceResponse shape is also asserted at the unit level.)
2222
test('makes no new-API calls that violate the OpenAPI spec', async ({ page }) => {
2323
await seedAuthenticatedSession(page);
24-
await page.route('**/workspaces/mine', EMPTY);
25-
await page.route('**/project-group-roles/**', EMPTY);
24+
await page.route(`${TEST_API_BASE}workspaces/mine`, EMPTY);
25+
await page.route(`${TEST_API_BASE}tdei-user/project-group-roles/**`, EMPTY);
2626

2727
const contract = recordContract(page);
2828
await page.goto('/dashboard');
@@ -34,8 +34,8 @@ test.describe('dashboard', () => {
3434
// @test e2e: the page renders with a simulated API response with no project groups
3535
test('shows the empty notice when the user has no project groups', async ({ page }) => {
3636
await seedAuthenticatedSession(page);
37-
await page.route('**/workspaces/mine', EMPTY);
38-
await page.route('**/project-group-roles/**', EMPTY);
37+
await page.route(`${TEST_API_BASE}workspaces/mine`, EMPTY);
38+
await page.route(`${TEST_API_BASE}tdei-user/project-group-roles/**`, EMPTY);
3939

4040
await page.goto('/dashboard');
4141

@@ -46,8 +46,8 @@ test.describe('dashboard', () => {
4646
test('shows the empty notice when the selected project group has no workspaces', async ({ page }) => {
4747
await seedAuthenticatedSession(page);
4848
await seedProjectGroupSelection(page, { id: PROJECT_GROUP_ID, name: 'Puget Sound' });
49-
await page.route('**/workspaces/mine', EMPTY); // group exists, but no workspaces in it
50-
await page.route('**/project-group-roles/**', route => route.fulfill({ json: projectGroups }));
49+
await page.route(`${TEST_API_BASE}workspaces/mine`, EMPTY); // group exists, but no workspaces in it
50+
await page.route(`${TEST_API_BASE}tdei-user/project-group-roles/**`, route => route.fulfill({ json: projectGroups }));
5151

5252
await page.goto('/dashboard');
5353

test/e2e/export-index.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ test.describe('workspace export landing page', () => {
131131

132132
release();
133133

134-
// Ready state: the button is replaced by a "Save" link.
134+
// Ready state: the button is replaced by a "Save" link. Assert the stable
135+
// link text/visibility only — its href is a volatile blob: URL, so we don't
136+
// ARIA-snapshot it (file-based aria snapshots capture the blob URL exactly).
135137
const save = page.getByRole('link', { name: /Save/ });
136138
await expect(save).toBeVisible();
137139

test/e2e/export-index.spec.ts-snapshots/workspace-export-landing-page-the-Download-flo-801cf--clicking-Save-downloads-the-file-snapshots-3.aria.yml

Lines changed: 0 additions & 2 deletions
This file was deleted.

test/e2e/export-tdei.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,10 @@ test('an API error shows an error message and a Try again button that resets the
245245
await stubPageLoad(page, [eligibleProjectGroup]);
246246
await stubServices(page);
247247

248-
await page.route('**/osm/workspaces/1/bbox.json', route =>
248+
await page.route('**/osm/api/0.6/workspaces/1/bbox.json', route =>
249249
route.fulfill({ json: { min_lat: 47.6, min_lon: -122.34, max_lat: 47.62, max_lon: -122.32 } })
250250
);
251-
await page.route('**/osm/map?**', route =>
251+
await page.route('**/osm/api/0.6/map?**', route =>
252252
route.fulfill({ body: '<osm></osm>', contentType: 'application/xml' })
253253
);
254254
await page.route('**/osw/convert', route => route.fulfill({ body: 'job-1', contentType: 'text/plain' }));
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
- alert:
22
- heading " An error occurred:" [level=5]
3-
- paragraph: "Unexpected error: TypeError: Failed to fetch (api.test)"
3+
- paragraph: "TDEI rejected the upload: Internal server error"
44
- button "Try again"

test/e2e/fixtures.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test as base, expect } from '@playwright/test';
22
import type { Page } from '@playwright/test';
3-
import { myWorkspaces, USER_ID } from '../mocks/fixtures';
3+
import { myWorkspaces, USER_ID, TEST_API_BASE } from '../mocks/fixtures';
44

55
// The display name shown in the navbar for the seeded session.
66
export const TEST_USER = { subject: USER_ID, displayName: 'Tester' };
@@ -42,12 +42,12 @@ export async function seedProjectGroupSelection(page: Page, group: { id: string;
4242
// test flag.
4343
export const test = base.extend({
4444
page: async ({ page }, use) => {
45-
await page.route('**/workspaces/mine', route =>
45+
await page.route(`${TEST_API_BASE}workspaces/mine`, route =>
4646
route.fulfill({ json: myWorkspaces })
4747
);
4848

4949
// Add further stubs here as flows need them, e.g.:
50-
// await page.route('**/workspaces/ws-1', route => route.fulfill({ json: ... }))
50+
// await page.route(`${TEST_API_BASE}workspaces/ws-1`, route => route.fulfill({ json: ... }))
5151

5252
await use(page);
5353
}

test/e2e/team-join.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { test, expect, seedAuthenticatedSession } from './fixtures';
2-
import { aWorkspace, USER_ID } from '../mocks/fixtures';
2+
import { aWorkspace, USER_ID, TEST_API_BASE } from '../mocks/fixtures';
33

44
// Generated from the @test outline in pages/workspace/[id]/teams/[teamId].vue
55
// (the "Join Team" page).
@@ -56,14 +56,14 @@ const joinedUser = {
5656

5757
// Stub the three top-level awaits. `members` controls join state.
5858
async function stubPageLoad(page: Page, members: object[]) {
59-
await page.route(`**/workspaces/${WORKSPACE_ID}`, route =>
59+
await page.route(`${TEST_API_BASE}workspaces/${WORKSPACE_ID}`, route =>
6060
route.fulfill({ json: aWorkspace })
6161
);
62-
await page.route(`**/workspaces/${WORKSPACE_ID}/teams/${TEAM_ID}`, route =>
62+
await page.route(`${TEST_API_BASE}workspaces/${WORKSPACE_ID}/teams/${TEAM_ID}`, route =>
6363
route.fulfill({ json: team })
6464
);
6565
// GET lists members; POST (joinTeam) returns the joining User. Same URL.
66-
await page.route(`**/workspaces/${WORKSPACE_ID}/teams/${TEAM_ID}/members`, (route) => {
66+
await page.route(`${TEST_API_BASE}workspaces/${WORKSPACE_ID}/teams/${TEAM_ID}/members`, (route) => {
6767
if (route.request().method() === 'POST') {
6868
return route.fulfill({ json: joinedUser });
6969
}

0 commit comments

Comments
 (0)