Conversation
- Add early return in initForProject to avoid redundant sessionStorage reads - Create shared selector functions in view-as-project-permissions.ts: - createViewAsCredentialsQuery: fetches deployment credentials for mocked user - createViewAsProjectQuery: fetches project with mocked user's JWT - computeEffectivePermissions: computes effective permissions based on View As state - Update project layout and TopNavigationBar to use shared selectors - Reduces code duplication in the 3-query chain pattern Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
Add Playwright tests for the View As functionality: - Admin can access View As menu from project home and dashboard - Admin can select a user to view as - View As state persists across page refresh - View As state persists when navigating within the same project - View As state clears when navigating to a different project - Admin can clear View As state using the chip remove button - Admin can change the viewed user using the chip dropdown Note: The test for effective permissions (e.g., Share button hidden for viewer) is commented out as it requires the viewerPage fixture to be fully set up. Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
|
Cursor Agent can help with this pull request. Just |
Lint rule vitest/no-commented-out-tests requires using skip/only instead of commenting out tests. Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
ericpgreen2
left a comment
There was a problem hiding this comment.
Issues
1. E2E tests don't validate the core behavior of View As (High)
The test suite tests UI mechanics (popover opens, chip renders, sessionStorage persists) but not the actual purpose of View As — that an admin sees restricted permissions when impersonating a less-privileged user.
The e2e environment only has one user (the admin), so every test selects the admin to "view as" themselves. The comment on line 72 makes this explicit: // Select the first user in the list (the admin user itself). Since the effective permissions are identical to the admin's real permissions, the query chain (GetDeploymentCredentials → GetProjectWithBearerToken → effective permissions) is never exercised in a way that produces observable differences.
The skipped test ("Effective permissions update correctly when viewing as a viewer") is the one that would cover this, but it requires a second user with viewer-level permissions to be provisioned in setup.ts. Without it, the tests give confidence that the UI plumbing works but not that security policy enforcement actually functions through View As.
Consider either:
- Provisioning a viewer user in the e2e setup and enabling the skipped test, or
- Adding a unit/integration test that asserts the effective permissions reflect the mocked user's restrictions (not the admin's) when View As is active.
2. The shared selectors are thin wrappers — consider a compound hook instead (Medium)
view-as-project-permissions.ts extracts three functions, but both consumers (+layout.svelte:138-158, TopNavigationBar.svelte:80-99) still repeat the same multi-step chain: read viewAsUserStore → create credentials query → pipe access token into project query → derive effective permissions. The wrappers move individual steps behind functions without actually reducing the duplication.
Worse, the two consumers compute effective permissions differently — the layout uses computeEffectivePermissions (switches on the whole object), while the navbar uses inline ?? fallback per field (falls through to the admin's permission if a mocked field is undefined).
A single compound hook that encapsulates the entire chain would eliminate both problems:
// Returns the mocked user's project permissions (or undefined when View As is inactive)
export function createViewAsPermissions(org: string, project: string) {
const mockedUserId = get(viewAsUserStore)?.id; // or accept as param
const credentialsQuery = createAdminServiceGetDeploymentCredentials(...);
const projectQuery = createAdminServiceGetProjectWithBearerToken(...);
// return derived store with projectPermissions
}Both consumers would call one thing and get back the mocked permissions, then merge with their actual permissions in a single consistent place.
3. Silent test skip inside "Admin can change the viewed user" (Low)
view-as.spec.ts:247-259: The if (userCount > 1) block means the core assertion (selecting a different user) is silently skipped when only one user exists — which is always the case in the current e2e environment. The test passes without testing its stated purpose. Consider asserting userCount > 1 so CI fails loudly if the fixture is misconfigured, or skip the test with a message.
Minor
test.describe.configure({ mode: "serial" })may not be needed — each test navigates fresh and doesn't depend on prior test state. Parallel mode would be faster in CI.
Developed in collaboration with Claude Code
1. Create compound hook (createViewAsState) that encapsulates the entire View As query chain: viewAsUserStore → GetDeploymentCredentials → GetProjectWithBearerToken → permissions 2. Update layout and TopNavigationBar to use the compound hook, eliminating code duplication and ensuring consistent permission handling 3. Fix e2e tests: - Remove unnecessary serial mode (tests don't depend on each other) - Add explicit test.skip when only one user exists instead of silent skip - Keep skipped test for effective permissions until viewer user is provisioned Note: Provisioning a viewer user in e2e setup requires additional environment variables and auth0 configuration - recommended for a follow-up PR. Co-authored-by: ericokuma <ericokuma@users.noreply.github.com>
ericokuma
left a comment
There was a problem hiding this comment.
The compound hook addresses the duplication well, and the test fixes are clean. One issue in the new implementation:
Subscription leak in createViewAsState (Medium)
Inside the credentialsQuery.subscribe callback (line 67), a new projectQuery subscription is created every time the credentials store emits — refetches, cache updates, background polling, etc. The return unsubProject on line 95 is the return value of the .subscribe callback, which Svelte ignores (only derived's outer function return is treated as a cleanup). So previous project subscriptions are never torn down and accumulate over the lifetime of the component.
Each leaked subscription calls set() on the derived store, causing redundant updates. TanStack Query deduplication prevents duplicate network requests, but the subscription count still grows unboundedly.
One way to fix it:
const unsubCredentials = credentialsQuery.subscribe(($credentialsQuery) => {
const accessToken = $credentialsQuery.data?.accessToken;
if (!accessToken) {
unsubProject?.();
unsubProject = undefined;
set({ ... });
return;
}
// Only re-subscribe if the token actually changed
if (accessToken === prevAccessToken) return;
prevAccessToken = accessToken;
unsubProject?.();
const projectQuery = createAdminServiceGetProjectWithBearerToken(...);
unsubProject = projectQuery.subscribe(($projectQuery) => {
set({ ... });
});
});
return () => {
unsubProject?.();
unsubCredentials();
};The key ideas: (1) track and clean up the inner subscription, (2) skip re-subscribing when the token hasn't changed, (3) clean up both subscriptions in the outer cleanup.
Developed in collaboration with Claude Code
This PR addresses the follow-up feedback for the "View As" feature (APP-765) by improving code structure, efficiency, and test coverage.
Key Changes:
GetDeploymentCredentials→GetProjectWithBearerToken→ effective permissions query chain into a new shared selector (useViewAsProjectPermissions). This deduplicates logic previously duplicated inProjectLayoutandTopNavigationBar, making the code more maintainable and reducing component complexity.viewerPagefixture setup).initForProject: Implemented an early return ininitForProjectto prevent redundantsessionStoragereads when the project scope has not changed, improving efficiency.Checklist:
Linear Issue: APP-765