Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ e2e-visual: ## Screenshots of the built viewer in every theme against the commit
npx playwright test --project visual $(VISUAL_FLAGS)

.PHONY: e2e-visual-update
e2e-visual-update: ## Take the screenshots again after a change of the look that is meant
e2e-visual-update: build ## Take the screenshots again after a change of the look that is meant
# Built first: references taken from a stale binary would not match the code
$(MAKE) e2e-visual VISUAL_FLAGS=--update-snapshots

.PHONY: ui-build
Expand Down
Binary file modified ui/e2e/__screenshots__/showcase.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions ui/e2e/viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ test('errors of a page come in as a child of its PHP request', async ({ page, re
const parent = feed.items.find((item) => item.url?.includes('/catalog/item'));
expect(parent).toBeDefined();
const ingest = `http://127.0.0.1:${Number(process.env.E2E_PORT ?? 18840) + 1}`;
// Every run names its page apart: the viewer keeps the children of runs before it
const run = Date.now().toString(36);

// The shop page as PHP would print it: Debugger::jsTag(), then a script that fails. A real local
// server: Chrome lets a page reach a local receiver only from the same address space.
Expand All @@ -190,15 +192,15 @@ test('errors of a page come in as a child of its PHP request', async ({ page, re
await new Promise<void>((done) => shop.listen(0, '127.0.0.1', done));
const { port } = shop.address() as AddressInfo;
try {
await page.goto(`http://127.0.0.1:${port}/catalog/item?id=42`);
await page.goto(`http://127.0.0.1:${port}/catalog/item?id=42&run=${run}`);
await page.waitForTimeout(300);
await page.goto('/'); // leaving the page sends the rest with a beacon
} finally {
shop.close();
}

const requests = page.getByRole('region', { name: 'Requests' });
const child = requests.getByRole('button', { name: /GET \/catalog\/item\?id=42.*1 error/ });
const child = requests.getByRole('button', { name: new RegExp(`run=${run}.*1 error`) });
await expect(child).toBeVisible({ timeout: 10_000 });
await child.click();
await expect(
Expand Down
20 changes: 20 additions & 0 deletions ui/src/lib/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,26 @@ describe('createHttpTransport', () => {
expect(source.close).toHaveBeenCalled();
});

it('tells subscribers to reload once the stream first opens', () => {
// What happened between the first load and the connection has no Last-Event-ID to come back by
const listeners = new Map<string, (event: MessageEvent) => void>();
const source: EventSourceLike = {
addEventListener: (name, listener) => listeners.set(name, listener),
close: vi.fn(),
};
const transport = createHttpTransport({
baseUrl: '',
fetch: vi.fn(),
createEventSource: () => source,
});
const received: unknown[] = [];
transport.subscribe((event) => received.push(event));
listeners.get('open')?.(new MessageEvent('open'));
// A reconnection sends Last-Event-ID and the server sends what was missed
listeners.get('open')?.(new MessageEvent('open'));
expect(received).toEqual([{ event: 'resync' }]);
});

it('shares one stream between subscribers and closes it after the last one leaves', () => {
const listeners = new Map<string, (event: MessageEvent) => void>();
const source: EventSourceLike = {
Expand Down
23 changes: 17 additions & 6 deletions ui/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,25 @@ export function createHttpTransport(options: HttpTransportOptions): Transport {
subscribers.add(onEvent);
if (!source) {
const opened = createEventSource(`${options.baseUrl}/api/v1/stream`);
const deliver = (event: StreamEvent) => {
for (const subscriber of [...subscribers]) {
subscriber(event);
}
};
for (const name of streamEventNames) {
opened.addEventListener(name, (message) => {
const event = JSON.parse(String(message.data)) as StreamEvent;
for (const subscriber of [...subscribers]) {
subscriber(event);
}
});
opened.addEventListener(name, (message) =>
deliver(JSON.parse(String(message.data)) as StreamEvent),
);
}
// Events between the first load and the first connection have no Last-Event-ID to come back
// by: the subscribers load again once. Reconnections get the missed events from the server.
let connected = false;
opened.addEventListener('open', () => {
if (!connected) {
connected = true;
deliver({ event: 'resync' });
}
});
source = opened;
}
return () => {
Expand Down
Loading