Build and Push Docker Image / build-and-push (push) Successful in 6m47s
- Admin: sign-up Open / Invite only / Closed with single-use invite links - Present mode for scripts (auto-scroll) and outlines (step through, pace vs planned minutes) - Series: group studies on the dashboard, filter by series, calendar file for a study's sessions - Lesson handout: printable page per session (passage, outline, questions, writing lines) - Save a chunk's notes as your own study template (synced) - Voice dictation for notes and scripts (browser speech recognition) - Reading progress: map of the 66 books and a calendar of days read, synced and merged across devices - Export highlights and bookmarks as Markdown or Word, grouped by named colour - Verse of the day push to the ntfy topic (needs IQBIBLE_API_KEY) - Sharing: co-teacher editing via single-use invite links; read-only links can expire and take a passcode Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
50 lines
2.5 KiB
React
50 lines
2.5 KiB
React
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { vi, test, expect, beforeEach } from 'vitest';
|
|
|
|
vi.mock('../syncService.js', () => ({ getCollaborators: vi.fn(), createEditInvite: vi.fn(), removeCollaborator: vi.fn() }));
|
|
import CoTeachers from './CoTeachers.jsx';
|
|
import { createEditInvite, getCollaborators, removeCollaborator } from '../syncService.js';
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
getCollaborators.mockResolvedValue({ ok: true, data: { collaborators: [{ userId: 'u2', email: 'sam@x.co' }], pendingInvites: 0 } });
|
|
createEditInvite.mockResolvedValue({ ok: true, data: { token: 'abc123', expiresAt: Date.now() + 7 * 86_400_000 } });
|
|
removeCollaborator.mockResolvedValue({ ok: true });
|
|
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
});
|
|
|
|
test('the owner sees the co-teachers and can make a single-use invite link', async () => {
|
|
const user = userEvent.setup();
|
|
render(<CoTeachers projectId="p1" />);
|
|
expect(await screen.findByText('sam@x.co')).toBeInTheDocument();
|
|
await user.click(screen.getByRole('button', { name: /invite a co-teacher/i }));
|
|
const link = await screen.findByLabelText('Invite link');
|
|
expect(link.value).toContain('?join=abc123');
|
|
expect(createEditInvite).toHaveBeenCalledWith('p1');
|
|
});
|
|
|
|
test('the owner can remove a co-teacher after confirming', async () => {
|
|
const user = userEvent.setup();
|
|
render(<CoTeachers projectId="p1" />);
|
|
await user.click(await screen.findByRole('button', { name: 'Remove' }));
|
|
expect(removeCollaborator).toHaveBeenCalledWith('p1', 'u2');
|
|
});
|
|
|
|
test('someone invited to a study sees who shared it and can leave', async () => {
|
|
const onLeave = vi.fn();
|
|
const user = userEvent.setup();
|
|
render(<CoTeachers projectId="p1" sharedBy="owner@x.co" onLeave={onLeave} />);
|
|
expect(screen.getByText(/owner@x.co invited you to edit/)).toBeInTheDocument();
|
|
expect(getCollaborators).not.toHaveBeenCalled(); // only owners can list co-teachers
|
|
await user.click(screen.getByRole('button', { name: /leave this study/i }));
|
|
expect(onLeave).toHaveBeenCalled();
|
|
});
|
|
|
|
test('when the server can\'t be reached, it says so instead of offering a link', async () => {
|
|
getCollaborators.mockResolvedValue({ ok: false, status: 404, error: 'Project not found.' });
|
|
render(<CoTeachers projectId="p1" />);
|
|
await waitFor(() => expect(screen.getByText(/Invites need your account/)).toBeInTheDocument());
|
|
expect(screen.queryByRole('button', { name: /invite a co-teacher/i })).toBeNull();
|
|
});
|