Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change unit tests to take advantage of new BasicAccordion component #1152

Merged
merged 1 commit into from
Nov 17, 2024
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
93 changes: 0 additions & 93 deletions src/resources/js/components/AccountServiceBodyList.svelte

This file was deleted.

3 changes: 2 additions & 1 deletion src/resources/js/components/BasicAccordion.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
export let header: string;
export let open: boolean = false;
export let label: string = 'Toggle accordion';

function accordionToggle() {
open = !open;
Expand All @@ -11,7 +12,7 @@
<button
type="button"
aria-expanded={open}
aria-label="Toggle accordion"
aria-label={label}
class="flex w-full items-center justify-between rounded-md px-4 py-4 text-left text-sm font-semibold text-gray-300 hover:bg-gray-700 focus:outline-none"
on:click={accordionToggle}
on:keydown={(e) => (e.key === 'Enter' || e.key === ' ') && accordionToggle()}
Expand Down
8 changes: 4 additions & 4 deletions src/resources/js/components/FormatForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@
{/if}
<div class="md:col-span-2">
{#each allLanguages as lang}
<BasicAccordion header={mappings[lang]} open={$translations.getLanguage() === lang}>
<BasicAccordion header={mappings[lang]} open={$translations.getLanguage() === lang} label={'Toggle accordion ' + lang}>
<div>
<Label for="{lang}_key" class="mb-2">{getLabel('keyTitle', lang)}</Label>
<Label for="{lang}_key" class="mb-2" aria-label="{lang} key">{getLabel('keyTitle', lang)}</Label>
<Input type="text" id="{lang}_key" name="{lang}_key" disabled={isReservedKey(lang)} />
<Helper class="mb-2" color="red">
{#if $errors[lang + '_key']}
Expand All @@ -207,7 +207,7 @@
</Helper>
</div>
<div>
<Label for="{lang}_name" class="mb-2">{getLabel('nameTitle', lang)}</Label>
<Label for="{lang}_name" class="mb-2" aria-label="{lang} name">{getLabel('nameTitle', lang)}</Label>
<Input type="text" id="{lang}_name" name="{lang}_name" />
<Helper class="mb-2" color="red">
{#if $errors[lang + '_name']}
Expand All @@ -216,7 +216,7 @@
</Helper>
</div>
<div>
<Label for="{lang}_description" class="mb-2">{getLabel('descriptionTitle', lang)}</Label>
<Label for="{lang}_description" class="mb-2" aria-label="{lang} description">{getLabel('descriptionTitle', lang)}</Label>
<Input type="text" id="{lang}_description" name="{lang}_description" />
<Helper class="mb-2" color="red">
{#if $errors[lang + '_description']}
Expand Down
82 changes: 78 additions & 4 deletions src/resources/js/routes/Account.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
<script lang="ts">
import { Button, Helper, Input, Label } from 'flowbite-svelte';
import { Button, Helper, Input, Label, Listgroup } from 'flowbite-svelte';
import { createEventDispatcher } from 'svelte';
import { createForm } from 'felte';
// svelte-hack' -- import hacked to get onMount to work correctly for unit tests
import { onMount } from 'svelte/internal';
import { validator } from '@felte/validator-yup';
import * as yup from 'yup';

import AccountServiceBodyList from '../components/AccountServiceBodyList.svelte';
import { authenticatedUser } from '../stores/apiCredentials';
import { formIsDirty } from '../lib/utils';
import Nav from '../components/NavBar.svelte';
import RootServerApi from '../lib/RootServerApi';
import { spinner } from '../stores/spinner';
import { translations } from '../stores/localization';
import type { User } from 'bmlt-root-server-client';
import type { ServiceBody, User } from 'bmlt-root-server-client';
import BasicAccordion from '../components/BasicAccordion.svelte';

let serviceBodies: ServiceBody[] = [];
let serviceBodiesLoaded = false;
let editableServiceBodyNames: string[] = [];

const dispatch = createEventDispatcher<{ saved: { user: User } }>();
let userType = 'unknown';
switch ($authenticatedUser?.type) {
Expand Down Expand Up @@ -129,6 +134,67 @@
}
}

async function getServiceBodies(): Promise<void> {
try {
spinner.show();
serviceBodies = await RootServerApi.getServiceBodies();
serviceBodiesLoaded = true;
} catch (error: any) {
await RootServerApi.handleErrors(error);
} finally {
spinner.hide();
}
}

// helper function to compute the set of service bodies that the currently logged in user can edit.
// s is the starting service body
// children is an array of sets of children, indexed by the id of the parent service body
// editableServiceBodies is the set of service bodies that is being accumulated
function recursivelyAddServiceBodies(s: ServiceBody, children: Set<ServiceBody>[], editableServiceBodies: Set<ServiceBody>) {
editableServiceBodies.add(s);
if (children[s.id]) {
for (const c of children[s.id]) {
recursivelyAddServiceBodies(c, children, editableServiceBodies);
}
}
}

onMount(() => {
getServiceBodies();
});

$: {
const id = $authenticatedUser?.id;
if (serviceBodiesLoaded && id) {
const editableServiceBodies: Set<ServiceBody> = new Set();
if ($authenticatedUser?.type === 'admin') {
serviceBodies.forEach((s) => editableServiceBodies.add(s));
} else if ($authenticatedUser?.type === 'serviceBodyAdmin') {
// children is an array with indices = service body ids, values a set of children of that service body
// (not recursively - the recursion is handled elsewhere)
const children: Set<ServiceBody>[] = [];
for (const s of serviceBodies) {
const p = s.parentId;
if (p) {
if (children[p]) {
children[p].add(s);
} else {
children[p] = new Set([s]);
}
}
}
for (const s of serviceBodies) {
if (s.adminUserId === id || s.assignedUserIds.includes(id)) {
recursivelyAddServiceBodies(s, children, editableServiceBodies);
}
}
}
editableServiceBodyNames = Array.from(editableServiceBodies)
.map((s) => s.name)
.sort();
}
}

$: isDirty.set(savedData ? formIsDirty(savedData, $data) : formIsDirty(initialValues, $data));
</script>

Expand Down Expand Up @@ -200,7 +266,15 @@
</div>
</div>
<BasicAccordion header={$translations.serviceBodiesWithEditableMeetings}>
<AccountServiceBodyList user={$authenticatedUser} />
{#if !serviceBodiesLoaded}
{$translations.loading}
{:else if editableServiceBodyNames.length === 0}
{$translations.none}
{:else}
<Listgroup items={editableServiceBodyNames} let:item>
{item}
</Listgroup>
{/if}
</BasicAccordion>
</div>
</form>
Expand Down
91 changes: 83 additions & 8 deletions src/resources/js/tests/Account.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeAll, beforeEach, describe, test } from 'vitest';
import { screen } from '@testing-library/svelte';
import { screen, waitFor } from '@testing-library/svelte';
import '@testing-library/jest-dom';

import { login, mockSavedUserPartialUpdate, sharedAfterEach, sharedBeforeAll, sharedBeforeEach } from './sharedDataAndMocks';
Expand Down Expand Up @@ -49,13 +49,8 @@ describe('check content in Account tab when logged in as various users', () => {
expect(mockSavedUserPartialUpdate?.description).toBe('Main Server Poobah');
expect(mockSavedUserPartialUpdate?.username).toBe('serverpoobah');
expect(mockSavedUserPartialUpdate?.password).toBe('new password');
// Mock clicking the expand icon is not causing the list to expand, so we can't test the list of editable service
// bodies. To work around this problem, the service body list is factored out into a separate component
// (AccountServiceBodyList), and tested separately.
// TODO: if we can get the simulated click on the expand icon to work, the separate AccountServiceBodyList
// component could be folded back in. Although the code is not too bad as is.
// const expand = screen.getByRole('button', { name: /service bodies this user can edit/i });
// await user.click(expand);
const expand = screen.getByRole('button', { name: /toggle accordion/i });
await user.click(expand);
// Now make a further change. The applyChanges button should be enabled again after a further change.
await user.clear(description);
await user.type(description, 'Main Server Imperial Wizard');
Expand Down Expand Up @@ -89,3 +84,83 @@ describe('check content in Account tab when logged in as various users', () => {
expect(mockSavedUserPartialUpdate?.password).toBe('poobah password');
});
});

describe('check lists of service bodies different users can edit', () => {
test('check toggling the accordion', async () => {
const user = await login('serveradmin', 'Account');
const toggle = await screen.findByRole('button', { name: /toggle accordion/i });
expect(toggle.ariaExpanded).toBe('false');
// TODO: this test fails -- toBeVisible seems to be always true, even if the accordion is collapsed
// expect(await screen.findByText('Northern Zone')).not.toBeVisible();
await user.click(toggle);
expect(toggle.ariaExpanded).toBe('true');
expect(await screen.findByText('Northern Zone')).toBeVisible();
});

test('check serveradmin account', async () => {
await login('serveradmin', 'Account');
await waitFor(() => {
expect(screen.queryByText('Northern Zone')).toBeInTheDocument();
expect(screen.queryByText('Big Region')).toBeInTheDocument();
expect(screen.queryByText('Small Region')).toBeInTheDocument();
expect(screen.queryByText('River City Area')).toBeInTheDocument();
expect(screen.queryByText('Mountain Area')).toBeInTheDocument();
expect(screen.queryByText('Rural Area')).toBeInTheDocument();
});
});

test('check Northern Zone admin', async () => {
await login('NorthernZone', 'Account');
await waitFor(() => {
expect(screen.queryByText('Northern Zone')).toBeInTheDocument();
expect(screen.queryByText('Big Region')).toBeInTheDocument();
expect(screen.queryByText('Small Region')).toBeInTheDocument();
expect(screen.queryByText('River City Area')).toBeInTheDocument();
expect(screen.queryByText('Mountain Area')).toBeInTheDocument();
expect(screen.queryByText('Rural Area')).toBeInTheDocument();
});
});

test('check Big Region admin', async () => {
await login('BigRegion', 'Account');
await waitFor(() => {
expect(screen.queryByText('Northern Zone')).toBe(null);
expect(screen.queryByText('Big Region')).toBeInTheDocument();
expect(screen.queryByText('Small Region')).toBe(null);
expect(screen.queryByText('River City Area')).toBeInTheDocument();
expect(screen.queryByText('Mountain Area')).toBeInTheDocument();
expect(screen.queryByText('Rural Area')).toBeInTheDocument();
});
});

test('check Big Region admin 2', async () => {
await login('BigRegion2', 'Account');
await waitFor(() => {
expect(screen.queryByText('Northern Zone')).toBe(null);
expect(screen.queryByText('Big Region')).toBeInTheDocument();
expect(screen.queryByText('Small Region')).toBe(null);
expect(screen.queryByText('River City Area')).toBeInTheDocument();
expect(screen.queryByText('Mountain Area')).toBeInTheDocument();
expect(screen.queryByText('Rural Area')).toBeInTheDocument();
});
});

test('check Small Region admin', async () => {
await login('SmallRegion', 'Account');
await waitFor(() => {
expect(screen.queryByText('Northern Zone')).toBe(null);
expect(screen.queryByText('Big Region')).toBe(null);
expect(screen.queryByText('Small Region')).toBeInTheDocument();
expect(screen.queryByText('River City Area')).toBe(null);
expect(screen.queryByText('Mountain Area')).toBe(null);
expect(screen.queryByText('Rural Area')).toBe(null);
});
});

test('check Small Region observer', async () => {
await login('SmallObserver', 'Account');
await waitFor(() => {
expect(screen.queryByText('- None -')).toBeInTheDocument();
});
});
});
Loading