From e3895d30b4328ee80b4a9e693bfd0022a670cd90 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Sat, 2 May 2026 02:37:01 -0700 Subject: [PATCH] feat: add view-as-role feature for site admins (#15) Site admins can now temporarily view the site as a player, tournament admin, or club admin to understand the experience for each role. Changes: - Added RoleSwitcher context and provider for client-side role simulation - Added role switcher dropdown in Navigation (visible only to site admins) - Added yellow banner showing current viewing-as role with reset button - Navigation links and wordmark href now use effective role for conditional display - Added BDD feature file with 5 scenarios covering all role transitions - Added step definitions for site admin login and role switcher interactions The view-as feature is purely UI-level - server-side permissions remain unchanged. 5 scenarios, 27 steps, all passing. --- e2e/cucumber/features/view-as-role.feature | 46 ++++ e2e/cucumber/step-definitions/auth-steps.ts | 52 ++++ e2e/cucumber/step-definitions/common-steps.ts | 58 +++++ src/app/layout.tsx | 7 +- src/components/Navigation.tsx | 234 +++++++++++------- src/components/RoleSwitcher.tsx | 37 +++ 6 files changed, 336 insertions(+), 98 deletions(-) create mode 100644 e2e/cucumber/features/view-as-role.feature create mode 100644 src/components/RoleSwitcher.tsx diff --git a/e2e/cucumber/features/view-as-role.feature b/e2e/cucumber/features/view-as-role.feature new file mode 100644 index 0000000..0df8e68 --- /dev/null +++ b/e2e/cucumber/features/view-as-role.feature @@ -0,0 +1,46 @@ +Feature: View As Role + As a site admin + I want to temporarily view the site as a player or club admin + So that I can understand and improve the experience for each role + + @happy-path @admin-features @issue-15 + Scenario: Site admin sees role switcher in navigation + Given I am logged in as a site admin + When I view the navigation + Then I should see the role switcher dropdown + Then the role switcher should default to "Viewing as Site Admin" + + @happy-path @admin-features @issue-15 + Scenario: Site admin switches to player view + Given I am logged in as a site admin + When I select "View as Player" from the role switcher + Then I should see the player navigation links + And I should not see the "Admin" link + And I should not see the "Users" link + And I should see a banner indicating I am viewing as "Player" + + @happy-path @admin-features @issue-15 + Scenario: Site admin switches to tournament admin view + Given I am logged in as a site admin + When I select "View as Tournament Admin" from the role switcher + Then I should see the "Tournaments" link + And I should not see the "Admin" link + And I should not see the "Users" link + And I should see a banner indicating I am viewing as "Tournament Admin" + + @happy-path @admin-features @issue-15 + Scenario: Site admin switches to club admin view + Given I am logged in as a site admin + When I select "View as Club Admin" from the role switcher + Then I should see the "Admin" link + And I should see the "Users" link + And I should see a banner indicating I am viewing as "Club Admin" + + @happy-path @admin-features @issue-15 + Scenario: Site admin resets to site admin view + Given I am logged in as a site admin + When I select "View as Player" from the role switcher + And I click the "Reset to Site Admin" button + Then the role switcher should default to "Viewing as Site Admin" + And I should see the "Admin" link + And I should not see the viewing as banner diff --git a/e2e/cucumber/step-definitions/auth-steps.ts b/e2e/cucumber/step-definitions/auth-steps.ts index 1637fc8..4f40d3e 100644 --- a/e2e/cucumber/step-definitions/auth-steps.ts +++ b/e2e/cucumber/step-definitions/auth-steps.ts @@ -158,6 +158,58 @@ Given('I am logged in as a tournament admin', async function () { console.log(`🌍 User created: ${credentials.email}`); }); +/** + * Precondition: I am logged in as a site admin + * Creates a new user and assigns site_admin role via Prisma + */ +Given('I am logged in as a site admin', async function () { + console.log('🌍 Creating and logging in as a site admin...'); + + const credentials = generateTestCredentials(); + world.user = credentials; + + await world.page.goto(`${world.baseURL}/auth/register`); + await world.page.waitForLoadState('domcontentloaded'); + + await world.page.fill('input[name="name"]', credentials.name); + await world.page.fill('input[name="email"]', credentials.email); + await world.page.fill('input[name="password"]', credentials.password); + + await world.page.click('button[type="submit"]'); + await world.page.waitForURL(/\/players\/\d+\/profile/, { timeout: 15000 }); + + const currentUrl = world.page.url(); + const match = currentUrl.match(/\/players\/(\d+)\/profile/); + if (match) { + const playerId = match[1]; + world.playerId = playerId; + + const prisma = await world.getPrisma(); + const player = await prisma.player.findUnique({ + where: { id: parseInt(playerId) }, + include: { user: true } + }); + + if (player && player.user) { + const userId = player.user.id; + (world.user as any).id = userId; + + await prisma.user.update({ + where: { id: userId }, + data: { role: 'site_admin' } + }); + console.log(`🌍 Assigned site_admin role to user: ${userId}`); + + // Navigate to home page to trigger Navigation re-mount with new role + await world.page.goto(`${world.baseURL}/`); + await world.page.waitForLoadState('networkidle'); + await world.page.waitForTimeout(1000); + } + } + + console.log(`🌍 Site admin created: ${credentials.email}`); +}); + /** * Precondition: I am logged in as a club admin * Uses a pre-existing admin user from the database diff --git a/e2e/cucumber/step-definitions/common-steps.ts b/e2e/cucumber/step-definitions/common-steps.ts index 56d4043..a27c840 100644 --- a/e2e/cucumber/step-definitions/common-steps.ts +++ b/e2e/cucumber/step-definitions/common-steps.ts @@ -690,3 +690,61 @@ Then('I should be on the match result entry page', async function () { console.log(`🌍 Checking current URL: ${currentUrl}`); expect(currentUrl).toMatch(/\/matches\/|\/admin\/tournaments\/\d+\/(entry|results)/); }); + +// View As Role Steps +When('I view the navigation', async function () { + await world.page.waitForLoadState('networkidle'); + await world.page.waitForTimeout(1000); + console.log('🌍 Viewing navigation'); +}); + +Then('I should see the role switcher dropdown', async function () { + const switcher = world.page.locator('[data-testid="role-switcher"]'); + await expect(switcher).toBeVisible({ timeout: 5000 }); + console.log('🌍 Verified role switcher dropdown is visible'); +}); + +Then('the role switcher should default to {string}', async function (expectedText: string) { + const switcher = world.page.locator('[data-testid="role-switcher"]'); + const selectedValue = await switcher.inputValue(); + const selectedText = await switcher.locator('option:checked').textContent(); + console.log(`🌍 Dropdown selected text: "${selectedText}", value: "${selectedValue}"`); + expect(selectedText?.trim()).toBe(expectedText); +}); + +When('I select {string} from the role switcher', async function (optionText: string) { + const switcher = world.page.locator('[data-testid="role-switcher"]'); + await switcher.selectOption({ label: optionText }); + await world.page.waitForTimeout(500); + console.log(`🌍 Selected "${optionText}" from role switcher`); +}); + +Then('I should see the player navigation links', async function () { + await expect(world.page.locator('nav a:has-text("Rankings")')).toBeVisible(); + await expect(world.page.locator('nav a:has-text("Tournaments")')).toBeVisible(); + console.log('🌍 Verified player navigation links are visible'); +}); + +Then('I should not see the {string} link', async function (linkText: string) { + const link = world.page.locator(`nav a:has-text("${linkText}")`); + await expect(link).not.toBeVisible({ timeout: 3000 }); + console.log(`🌍 Verified "${linkText}" nav link is not visible`); +}); + +Then('I should see the {string} link', async function (linkText: string) { + const link = world.page.locator(`nav a:has-text("${linkText}")`); + await expect(link).toBeVisible({ timeout: 5000 }); + console.log(`🌍 Verified "${linkText}" nav link is visible`); +}); + +Then('I should see a banner indicating I am viewing as {string}', async function (roleName: string) { + const banner = world.page.locator(`text=Viewing as ${roleName}`); + await expect(banner).toBeVisible({ timeout: 5000 }); + console.log(`🌍 Verified viewing as ${roleName} banner is visible`); +}); + +Then('I should not see the viewing as banner', async function () { + const banner = world.page.locator('[data-testid="reset-view-as"]'); + await expect(banner).not.toBeVisible({ timeout: 3000 }); + console.log('🌍 Verified viewing as banner is not visible'); +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 69b7a94..afefe17 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import "./globals.css"; import { SessionProvider } from "@/components/SessionProvider"; +import { RoleSwitcherProvider } from "@/components/RoleSwitcher"; import Footer from "@/components/Footer"; const inter = { @@ -24,8 +25,10 @@ export default function RootLayout({ > - {children} -