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
53 changes: 53 additions & 0 deletions src/Exceptionless.Web/ClientApp/e2e/tests/project-scoping.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,58 @@ test('operator can scope Events to a project and clear the project filter', asyn

await expect(page).not.toHaveURL(/[?&]project=/);
await expect(getVisibleText(page, e2eSecondaryProject.message)).toBeVisible({ timeout: 30_000 });

await page.goBack();
await expect(page).toHaveURL(new RegExp(`[?&]project=${e2eScenario.projectId}(?:&|$)`));
await expect(page.getByRole('button', { name: new RegExp(`^Project\\s+${escapeRegExp(e2eScenario.projectName)}`) })).toBeVisible();
await expect(getVisibleText(page, e2eSecondaryProject.message)).toBeHidden({ timeout: 30_000 });

await page.goForward();
await expect(page).not.toHaveURL(/[?&]project=/);
await expect(getVisibleText(page, e2eSecondaryProject.message)).toBeVisible({ timeout: 30_000 });
});
});

test('project scope on Most Frequent Errors survives immediate reload and history traversal', async ({ e2eApi, e2eScenario, page }) => {
await test.step('seed an error and scope the stack list from its details', async () => {
await seedRepresentativeEvent(e2eApi, e2eScenario.userToken, {
message: e2eScenario.message,
projectId: e2eScenario.projectId,
projectToken: e2eScenario.projectToken,
referenceId: e2eScenario.referenceId
});

await page.goto('/next/stack/most-frequent-errors');
const stackRow = getVisibleRow(page, e2eScenario.message);
await expect(stackRow).toBeVisible({ timeout: 30_000 });
await stackRow.click();

const stackSheet = page.getByRole('dialog', { name: 'Stack' });
await expect(stackSheet).toBeVisible();
await stackSheet.getByTitle(`Filter project:${e2eScenario.projectId}`).click();
});

await test.step('retain scope through an immediate reload', async () => {
await page.reload();

await expect(page).toHaveURL(new RegExp(`[?&]project=${e2eScenario.projectId}(?:&|$)`));
await expect(page.getByRole('button', { name: new RegExp(`^Project\\s+${escapeRegExp(e2eScenario.projectName)}`) })).toBeVisible();
await expect(page.getByTitle('Refresh results').locator('svg')).not.toHaveClass(/animate-spin/);
});

await test.step('restore the scoped and unscoped states through Back and Forward', async () => {
const projectFilter = page.getByRole('button', { name: new RegExp(`^Project\\s+${escapeRegExp(e2eScenario.projectName)}`) });
await projectFilter.click();
await expect(projectFilter).toHaveAttribute('aria-expanded', 'true');
await page.getByRole('button', { name: 'Remove filter' }).click();
await expect(page).not.toHaveURL(/[?&]project=/);

await page.goBack();
await expect(page).toHaveURL(new RegExp(`[?&]project=${e2eScenario.projectId}(?:&|$)`));
await expect(projectFilter).toBeVisible();

await page.goForward();
await expect(page).not.toHaveURL(/[?&]project=/);
await expect(projectFilter).toBeHidden();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ Exceptionless's shared Svelte query-parameter state module. It is intentionally
- top-level string, number, boolean, date, and enum parameters;
- typed property access and atomic multi-parameter updates;
- preservation of unrelated URL parameters;
- debounced push or replace navigation;
- immediate, reload-safe URL synchronization;
- coalescing of rapid push-history updates into one Back-button entry;
- synchronization with browser navigation;
- no state, URL, or history writes for unchanged values after coercion;
- cancellation of pending synchronization during navigation and component teardown.
- settlement of history-entry coalescing during navigation and component teardown.

```ts
import { createQueryParameters } from '$shared/query-params';
Expand All @@ -27,4 +28,6 @@ queryParams.update({ filter: 'status:open', page: 1 });

Updates may also assign a single schema property directly. Use `update()` when several parameters form one logical state change so they produce one reactive update and one URL synchronization.

With `history: 'push'`, the first update immediately pushes a durable Back-button entry. Rapid follow-up updates are coalesced into one replacement after `debounceMilliseconds`, avoiding browser History API mutation limits, and a pending replacement is flushed before reload, link navigation, or teardown. If a burst returns to its starting URL, comparison is independent of query encoding and parameter order, and the return is pushed so the transient state remains a meaningful Back target rather than creating adjacent duplicate URLs. Immediate Back keeps a pending replacement associated with its unique source entry; Forward restores the latest value after route teardown or reload without rewriting the Back destination or replaying it onto a later visit to the same URL. Session-storage persistence is best-effort; if browser policy denies storage, current-page coalescing continues without blocking hydration. With `history: 'replace'`, every update immediately replaces the current entry.

The implementation was originally derived from [beynar/kit-query-params](https://github.com/beynar/kit-query-params) version 0.0.26 at commit `7c90edf7`. The original copyright and MIT license are retained in [LICENSE](./LICENSE). This module is maintained as first-party Exceptionless code and does not track the upstream package API.
Loading
Loading