Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
dcbd667
test(e2e): add visitor invite coverage for workplace
sharmilaseenivasan17 Sep 15, 2026
d44c227
test(e2e): add room booking coverage for workplace
sharmilaseenivasan17 Sep 15, 2026
2edaa7d
test(e2e): cover editing, capacity, favourites, approval and catering…
sharmilaseenivasan17 Sep 16, 2026
5d0ac4f
test(e2e): cover Your Bookings and the desk gaps
sharmilaseenivasan17 Sep 16, 2026
b2dfe8d
test(e2e): add parking booking coverage for workplace
sharmilaseenivasan17 Sep 16, 2026
107909d
test(e2e): cover the workplace home page
sharmilaseenivasan17 Sep 16, 2026
5958d64
test(e2e): room, visitor and parking gaps, and three more findings
sharmilaseenivasan17 Sep 16, 2026
b0229a6
test(e2e): parking access and requests, desk level scoping, room feat…
sharmilaseenivasan17 Sep 16, 2026
97a79ac
test(e2e): moving a group invite moves every member (VIS-26)
sharmilaseenivasan17 Sep 16, 2026
3cc08bd
test(e2e): extend workplace and concierge app coverage
sharmilaseenivasan17 Sep 18, 2026
0254ce7
test(e2e): cover remaining concierge scenarios on 2026-09-18
sharmilaseenivasan17 Sep 18, 2026
76a2442
test(workplace): add booking limit and desk initialisation coverage
sharmilaseenivasan17 Sep 22, 2026
a2fddce
test(workplace): fix calendar navigation and empty booking coverage
sharmilaseenivasan17 Sep 22, 2026
cce609a
test(workplace): add group desk booking coverage
sharmilaseenivasan17 Sep 23, 2026
a5d1f65
test(workplace): add OAuth refresh chain coverage
sharmilaseenivasan17 Sep 23, 2026
0faf4a0
test(e2e): add isolated day for booking history coverage
sharmilaseenivasan17 Sep 24, 2026
2154508
test(workplace): cover mock room availability booking modal
sharmilaseenivasan17 Sep 24, 2026
e7a507b
test(room): return native room booking from shared flow
sharmilaseenivasan17 Sep 24, 2026
665a9df
test(room): defer unconfirmed multi-room coverage
sharmilaseenivasan17 Sep 24, 2026
3778bce
test(workplace): cover parking request restrictions
sharmilaseenivasan17 Sep 24, 2026
6f2491f
test(workplace): cover desk favourites filtering
sharmilaseenivasan17 Sep 29, 2026
704a960
test(workplace): cover parking favourites filtering
sharmilaseenivasan17 Sep 29, 2026
535c1d5
test(workplace): cover favourite room booking from home
sharmilaseenivasan17 Sep 29, 2026
6eb77c4
Merge remote-tracking branch 'origin/develop' into test/e2e-workplace…
sharmilaseenivasan17 Oct 2, 2026
14962f6
test(e2e): address PR review feedback and revalidate coverage
sharmilaseenivasan17 Oct 3, 2026
0717cf3
test(e2e): lift the guards that pass now and align the seeds and titl…
camreeves Oct 7, 2026
125ea04
test(e2e): cover visitor rebooking after early checkout
sharmilaseenivasan17 Oct 7, 2026
1066b94
Merge branch 'test/e2e-workplace-concierge-2026-09-18' of https://git…
sharmilaseenivasan17 Oct 7, 2026
8546c15
test(e2e): run the room status drivers on seeded rooms and lift HOME-10
camreeves Oct 7, 2026
36232ce
test(e2e): run the concierge rows that needed a default, a name and a…
camreeves Oct 7, 2026
095a72a
test(concierge): assert the open access default for CON-B1
camreeves Oct 8, 2026
7c4d7e5
test(concierge): ask the stack whether the calendar is backed
camreeves Oct 8, 2026
81ebc75
Merge branch 'develop' into test/e2e-workplace-concierge-2026-09-18
camreeves Oct 12, 2026
d704a5b
test(e2e): take the cross-worker races and vacuous checks out of the …
camreeves Oct 12, 2026
139f90f
test(e2e): put the helper doc comments back on their functions
camreeves Oct 12, 2026
b131c26
test(e2e): check YB-12 across every page of the listing
camreeves Oct 12, 2026
c7ffc3f
test(e2e): tighten the review fixes after a second read
camreeves Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 177 additions & 12 deletions E2E_USER_STORIES.md

Large diffs are not rendered by default.

206 changes: 206 additions & 0 deletions apps/concierge/e2e/local/concierge-access.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* CON-AUTH-01 / 02 / 03 and CON-B1 — who may open concierge.
*
* Concierge reads and writes everybody's bookings and the org hierarchy, and
* every other concierge spec signs in as an admin, so this file is the only
* place the access boundary is exercised.
*
* The boundary is `AuthorisedUserGuard`, driven by one setting,
* `app.allow_access_groups`. With a group named, a user outside it is sent to
* `/unauthorised` (CON-AUTH-02). With none named, which is the default and the
* state of this stack, every signed-in user is admitted (CON-B1). The open
* default is the intended behaviour, confirmed on PlaceOS/user-interfaces#551.
* The management pages are withheld from non-admins either way: the sidebar's
* `is_admin` reads the user's flags and groups, not the access setting.
*/
import { test, expect } from '../../../../e2e/support/concierge/fixtures';
import { CONCIERGE_URL } from '../../../../e2e/support/concierge/concierge.env';
import {
useSettings,
REQUIRE_ACCESS_GROUP,
} from '../../../../e2e/support/concierge/concierge.settings';

/** Sidebar links that only an admin should ever be offered. */
const ADMIN_ONLY = 'a[href*="zone-management"], a[href*="room-management"]';

const APP_HREF = new URL(CONCIERGE_URL).href;

/**
* Is the page still on the app? Matched on the app's full URL prefix, since
* the authority's login page can share the app's origin when the app is served
* from the stack. `about:blank` is the page before any navigation, so it counts.
*/
function onApp(url: string): boolean {
return url.startsWith('about:') || url.startsWith(APP_HREF);
}

test.describe('concierge access', () => {
test('CON-AUTH-01: an admin reaches the app and the shell renders', async ({
adminPage,
}) => {
await adminPage.goto('/#/');
// `app-topbar` and `app-sidebar` are the shell the existing mock specs
// already rely on, so they are the safest handles in this app.
await expect(
adminPage.locator('app-topbar'),
'the concierge shell never rendered for an admin. If this fails first, ' +
'nothing else in this app can be trusted. Check three things in order: ' +
`the app is served on ${CONCIERGE_URL}; the dev server's proxy is ` +
'pointed at the LOCAL stack via PLACE_PROXY_DOMAIN (see the webServer ' +
'block in playwright.config.ts — without it the app authenticates ' +
'against the shared dev deployment and is bounced to real Microsoft); ' +
'and the storage state was minted for THAT origin',
).toBeVisible({ timeout: 45_000 });
await expect(
adminPage.locator('app-sidebar'),
'the sidebar is what carries the navigation between concierge areas',
).toBeVisible({ timeout: 30_000 });
await expect(
adminPage,
'and an authorised admin must not be bounced to /unauthorised',
).not.toHaveURL(/unauthorised/, { timeout: 10_000 });
// The admin half of the comparison CON-B1 completes: an admin is
// offered the management pages, so their absence for staff means
// something.
await expect(
adminPage.locator(ADMIN_ONLY).first(),
'an admin should be offered the org-management pages',
).toBeVisible({ timeout: 20_000 });
});

test('CON-AUTH-02: with an access group set, a user outside it is refused', async ({
browser,
conciergeStaffState,
}) => {
// The guard only compares groups once one is named, so the override
// is what makes this a test of the guard. Applied to this context
// alone: the group branch has no sys_admin bypass, so suite-wide it
// would refuse the admin too (see REQUIRE_ACCESS_GROUP).
const context = await browser.newContext({
storageState: conciergeStaffState,
ignoreHTTPSErrors: true,
baseURL: CONCIERGE_URL,
});
try {
const page = await context.newPage();
await useSettings(page, REQUIRE_ACCESS_GROUP);
await page.goto('/#/');

// The guard calls `router.navigate(['/unauthorised'])`.
await expect(
page,
'with app.allow_access_groups set to a group this user is not in, ' +
'AuthorisedUserGuard must send them to /unauthorised. If this ' +
'fails, the guard is not refusing anyone under ANY configuration',
).toHaveURL(/unauthorised/, { timeout: 45_000 });
await expect(
page.locator(ADMIN_ONLY),
'and a refused user must not be offered the management pages',
).toHaveCount(0);
} finally {
await context.close();
}
});

test('CON-AUTH-03: an unauthenticated visit is sent to the authority login', async ({
browser,
}) => {
// `storageState: undefined` IS LOAD-BEARING. Do not remove it.
//
// Playwright merges the test's context options into
// `browser.newContext()`, and this suite's fixtures set `storageState`
// to the ADMIN state for every test. So a context created without
// saying otherwise silently carries admin credentials.
//
// Measured 2026-09-17: the first version of this test omitted it, and
// the "unauthenticated" context came up holding
// `<client_id>_access_token` and walked straight into `#/book/rooms`.
// The test then failed for the right-looking reason — "it stayed on the
// app" — while actually exercising the admin. A false FAILURE rather
// than a false pass, which is luckier than it deserved.
//
// The other tests here pass `conciergeStaffState` explicitly, so an
// explicit value wins and they were never affected.
const context = await browser.newContext({
storageState: undefined,
ignoreHTTPSErrors: true,
baseURL: CONCIERGE_URL,
});
try {
const page = await context.newPage();
await page.goto('/#/');
// ts-client finds no token, fails to refresh, and hands over to the
// authority's `login_url`. On this stack that is the local backend
// (`https://localhost:9443/login?continue=…`).
//
// Asserting "left the app" rather than one exact URL: where it lands
// depends on the authority's configured login_url and its auth
// source, which is a deployment choice, not a product guarantee. What
// must NOT happen is the concierge UI rendering for someone with no
// credentials at all.
await expect
.poll(() => onApp(page.url()), {
message:
'an unauthenticated visit must not stay on the concierge app. ' +
'If it does, check whether a token was left in this origin\'s ' +
'localStorage by another test — this context is deliberately ' +
'created without storage state',
timeout: 45_000,
})
.toBe(false);

await expect(
page.locator(ADMIN_ONLY),
'and no management surface may be rendered on the way out',
).toHaveCount(0);
} finally {
await context.close();
}
});

/**
* CON-B1 — the default configuration admits any signed-in user.
*
* `e2e-staff-0@place.tech` has `sys_admin: false`, `support: false` and
* `groups: []`. With `app.allow_access_groups` unset the guard admits it;
* the sidebar still drops the management block, because `is_admin` reads
* the user's flags and groups.
*/
test('CON-B1: with no access group configured, a staff user is admitted without the management pages', async ({
browser,
conciergeStaffState,
}) => {
const context = await browser.newContext({
storageState: conciergeStaffState,
ignoreHTTPSErrors: true,
baseURL: CONCIERGE_URL,
});
try {
const page = await context.newPage();
// No settings override: this is the default configuration.
await page.goto('/#/');
await expect(
page.locator('app-topbar'),
'a signed-in staff user must be given the concierge shell when no ' +
'access group is configured. If this fails, check nothing has ' +
'set app.allow_access_groups on this stack before reading the ' +
'guard: the open default is intended (PlaceOS/user-interfaces#551)',
).toBeVisible({ timeout: 45_000 });
await expect(
page.locator('app-sidebar a').first(),
'and the sidebar carries the booking areas for an admitted user',
).toBeVisible({ timeout: 30_000 });
await expect(
page,
'an admitted user must not be bounced to /unauthorised',
).not.toHaveURL(/unauthorised/);
await expect(
page.locator(ADMIN_ONLY),
'the management pages stay admin-only: app-sidebar hides the ' +
'facilities block for anyone without the admin flag or groups',
).toHaveCount(0);
} finally {
await context.close();
}
});
});
89 changes: 89 additions & 0 deletions apps/concierge/e2e/local/concierge-assets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* CON-ASSET-01 — concierge's asset list.
*
* ## This page does NOT use `simple-table`
*
* Measured 2026-09-17: `/#/book/assets/list/items` renders `asset-listing` and
* `app-asset-item-list`, and there is no `[role=table]` anywhere on it. So the
* shared `simple-table.page.ts` helper is useless here and the assertions read
* the component's own text instead. That difference is why the plan had this
* area down as needing a page object of its own.
*
* It groups by CATEGORY and then lists asset TYPES inside it — "E2E Equipment
* (1 items)" with "E2E AV Equipment" under it — rather than listing individual
* assets. So the thing to assert is that a seeded category and its type appear,
* not that three projector units do.
*
* ## It reuses the equipment fixture
*
* `asset.seed.ts` (from the workplace ROOM-24 work) creates a VISIBLE category
* `E2E Equipment` with type `E2E AV Equipment` and three units. That is exactly
* what this page should show, so there is no second seeder — and the fact that
* a fixture created for the workplace suite shows up correctly in concierge is
* itself worth one assertion.
*/
import { expect, test } from '../../../../e2e/support/concierge/fixtures';
import {
ASSET_CATEGORY,
ASSET_TYPE,
ensureEquipment,
} from '../../../../e2e/support/room/asset.seed';

const ASSET_LIST_ROUTE = '/#/book/assets/list/items';

test.describe('concierge assets', () => {
test('CON-ASSET-01: the asset list shows the seeded category and its type', async ({
adminPage,
adminApi,
}) => {
// `ensureEquipment` mints its own admin context from the WORKPLACE token
// file, which the concierge fixtures do not write — so the catalogue is
// read through this suite's own admin instead, and only created if it is
// genuinely absent.
const existing = await adminApi.get('/api/engine/v2/asset_types', {
params: { limit: '500' },
});
const body = await existing.json();
const types = (Array.isArray(body) ? body : (body.data ?? body.results ?? [])) as Array<
Record<string, any>
>;
if (!types.some((type) => type.name === ASSET_TYPE)) {
await ensureEquipment();
}

const crashes: string[] = [];
adminPage.on('pageerror', (error) => crashes.push(error.message));

await adminPage.goto(ASSET_LIST_ROUTE);

const listing = adminPage.locator('app-asset-item-list');
await expect(
listing,
'the asset list never rendered. Note this page uses `asset-listing` / ' +
'`app-asset-item-list`, NOT simple-table, so the shared table helper ' +
'does not apply',
).toBeVisible({ timeout: 45_000 });

await expect
.poll(async () => (await listing.innerText().catch(() => '')).replace(/\s+/g, ' '), {
message:
`the asset list should show the seeded category "${ASSET_CATEGORY}" ` +
`and the type "${ASSET_TYPE}" inside it. The list groups by ` +
`CATEGORY and lists TYPES, not individual units, so it will not ` +
`name the three projector assets`,
timeout: 30_000,
})
.toContain(ASSET_CATEGORY);

const text = (await listing.innerText()).replace(/\s+/g, ' ');
expect(
text,
`and the type inside that category. Full text: ${text.slice(0, 200)}`,
).toContain(ASSET_TYPE);

expect(
crashes,
'the asset list must not throw while rendering',
).toEqual([]);
});
});
Loading
Loading