+
+
S'inscrire
+ {
+ errors.username_error = null;
+ }}
+ type="text"
+ bind:value={$username.value}
+ label="Nom d'utilisateur"
+ errors={[
+ ...errorMsg($myForm, 'username'),
+ ...(errors.username_error != null ? [errors.username_error] : [])
+ ]}
+ />
+ {
+ errors.password_error = null;
+ }}
+ type="password"
+ bind:value={$password.value}
+ label="Mot de passe"
+ errors={[
+ ...errorMsg($myForm, 'password'),
+ ...(errors.password_error != null ? [errors.password_error] : [])
+ ]}
+ />
+ {
+ errors.confirm_error = null;
+ }}
+ bind:value={$confirm.value}
+ label="Confirmation"
+ errors={[
+ ...errorMsg($myForm, 'confirm'),
+ ...(errors.confirm_error != null ? [errors.confirm_error] : [])
+ ]}
+ />
+ {
+ register($username.value, $password.value, $confirm.value).catch((r) => {
+ console.log('ERREUR', r);
-
\ No newline at end of file
+
diff --git a/frontend/src/store/ws.ts b/frontend/src/store/ws.ts
new file mode 100644
index 0000000..54e7e0b
--- /dev/null
+++ b/frontend/src/store/ws.ts
@@ -0,0 +1,33 @@
+import { browser } from '$app/environment';
+import { writable, get } from 'svelte/store';
+import ReconnectingWebSocket from 'reconnecting-websocket';
+export const messages = writable([]);
+export const handlers = writable({})
+export const events = writable([])
+export const connect = (url,) => {
+ if (!browser) return {send: (e)=>{}, close: (e)=>{}};
+ const ws = new ReconnectingWebSocket(url);
+
+ ws.onmessage = (m) => {
+ console.log('MESAGE', m)
+
+ messages.update((o) => [JSON.parse(m.data), ...o]);
+ Object.values(get(handlers)).map(h => {
+ if(h ==null)return
+ h(JSON.parse(m.data));
+ })
+ };
+ ws.onopen = (e) => {
+ events.update((o)=>[{type: 'open', e}, ...o])
+ }
+ ws.onerror = (e) => {
+ events.update((o)=>[{type: 'error', e}, ...o])
+ }
+ ws.onclose = (e) => {
+ events.update((o)=>[{type: 'close', e}, ...o])
+ }
+
+ return {
+ send: (d) => { ws.send(JSON.stringify(d)) }, close: (code: number) => ws.close(code), ws
+ }
+};
diff --git a/frontend/src/test/ContextTest.svelte b/frontend/src/test/ContextTest.svelte
new file mode 100644
index 0000000..f202de6
--- /dev/null
+++ b/frontend/src/test/ContextTest.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/test/exo.test.ts b/frontend/src/test/exo.test.ts
new file mode 100644
index 0000000..3d370c8
--- /dev/null
+++ b/frontend/src/test/exo.test.ts
@@ -0,0 +1,922 @@
+import { render, fireEvent, createEvent } from '@testing-library/svelte';
+import { writable } from 'svelte/store';
+import { describe, expect, test, vi } from 'vitest';
+import Card from '../components/exos/Card.svelte';
+import ContextTest from './ContextTest.svelte';
+import type { Exercice } from '../types/exo.type';
+import DownloadForm from '../components/exos/DownloadForm.svelte';
+import ModalCard from '../components/exos/ModalCard.svelte';
+import Head from '../components/exos/Head.svelte';
+import TagContainer from '../components/exos/TagContainer.svelte';
+import Tag from 'src/components/exos/Tag.svelte';
+import EditForm from 'src/components/exos/EditForm.svelte';
+import FileInput from 'src/components/forms/FileInput.svelte';
+import Pagination from 'src/components/exos/Pagination.svelte';
+const auth = { key: 'auth', value: { isAuth: writable(true) } };
+const modal = {
+ key: 'modal',
+ value: {
+ show: (c, o) => {},
+ close: () => {}
+ }
+};
+const nav = { key: 'navigation', value: { navigate: () => {} } };
+const notif = {
+ key: 'notif',
+ value: { alert: () => {}, info: () => {}, success: () => {}, error: () => {} }
+};
+const alert = { key: 'alert', value: { alert: () => {} } };
+describe('Exo card', () => {
+ const contexts = [auth, nav, modal, notif, alert];
+
+ const baseExo: Exercice = {
+ csv: true,
+ pdf: true,
+ web: true,
+ name: 'Title',
+ consigne: 'The consigne',
+ author: { username: 'ouioui' },
+ private: false,
+ id_code: 'ABCDEF',
+ exo_source: '/test',
+ original: null,
+ examples: {
+ type: 'csv',
+ data: [
+ { calcul: '1+...=2', correction: '2' },
+ { calcul: '1+...=2', correction: '2' },
+ { calcul: '1+...=2', correction: '2' }
+ ]
+ },
+ is_author: true,
+ tags: []
+ };
+
+ test('Copy', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ card.getByTestId('copy');
+ });
+ test('TagEditing', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts: [
+ ...contexts,
+ {
+ key: 'modal',
+ value: {
+ show: () => {
+ expect(true).toBeFalsy();
+ }
+ }
+ },
+ { key: 'tags', value: writable({ data: [], isLoading: false }) }
+ ],
+ props: {
+ exo: baseExo
+ }
+ }
+ });
+ const expand = card.getByText('+', { exact: true });
+ fireEvent.click(expand).then(() => {
+ card.getByRole('button', { name: 'Annuler' });
+ card.getByRole('button', { name: 'Valider !' });
+ const tags = card.container.querySelector('.svelecte');
+ expect(tags).not.toBeNull();
+ });
+ });
+
+ test('No auth', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts: [nav, notif, modal, { key: 'auth', value: { isAuth: writable(false) } }],
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ expect(card.queryByTestId('copy')).toBeNull();
+ expect(card.queryByTestId('tags')).toBeNull();
+ card.getByText((c, e) => {
+ return c == 'Par' && e?.children[0].innerHTML == 'ouioui';
+ });
+ });
+
+ test('Example', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: { ...baseExo }
+ }
+ }
+ });
+
+ const exs = card.queryAllByText(baseExo.examples.data[0].calcul);
+ expect(exs.length).toBe(3);
+ });
+
+ test('Check author', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ card.getByText((c, e) => {
+ return c == 'Par' && e?.children[0].innerHTML == 'ouioui';
+ });
+ });
+
+ test('Check title', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: baseExo
+ }
+ }
+ });
+
+ card.getByText('Title');
+ });
+ test('Check consigne', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: baseExo
+ }
+ }
+ });
+
+ card.getByText('The consigne');
+ });
+
+ test('Check no consigne', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: { ...baseExo, consigne: null }
+ }
+ }
+ });
+ expect(card.queryByTestId('consigne')).toBeNull();
+ });
+
+ test('Show', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts: [
+ ...contexts,
+ {
+ key: 'modal',
+ value: {
+ show: (c, o) => {
+ console.log(c, o);
+ expect(c).toBe(ModalCard);
+ expect(o.exo).toBe(baseExo);
+ }
+ }
+ },
+ {
+ key: 'navigation',
+ value: {
+ navigate: (url) => {
+ expect(url).toBe('/exercices/' + baseExo.id_code);
+ }
+ }
+ }
+ ],
+ props: {
+ exo: baseExo
+ }
+ }
+ });
+
+ const cardElement = card.container.querySelector('.card');
+
+ fireEvent.click(cardElement).then(() => {
+ console.log('LOCALTE', document.location.toString());
+ });
+ });
+
+ test('Public mark', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: { ...baseExo, private: false }
+ }
+ }
+ });
+
+ card.getByText('Public');
+ expect(card.queryByText('Privé')).toBeNull();
+ });
+
+ test('Private mark', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: { ...baseExo, private: true }
+ }
+ }
+ });
+ card.getByText('Privé');
+ expect(card.queryByText('Public')).toBeNull();
+ });
+
+ test('tags', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: Card,
+ contexts,
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ card.getByTestId('tags');
+ });
+});
+
+describe('Modal card download', () => {
+ const contexts = [auth, nav, modal, notif, alert];
+
+ const baseExo: Exercice = {
+ supports: { csv: true, pdf: true, web: true },
+ name: 'Title',
+ consigne: 'The consigne',
+ author: { username: 'ouioui' },
+ private: false,
+ id_code: 'ABCDEF',
+ exo_source: '/test',
+ original: null,
+ examples: {
+ type: 'csv',
+ data: [
+ { calcul: '1+...=2', correction: '2' },
+ { calcul: '1+...=2', correction: '2' },
+ { calcul: '1+...=2', correction: '2' }
+ ]
+ },
+ is_author: true,
+ tags: []
+ };
+
+ test('Copy', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ card.getByTestId('copy');
+ });
+
+ test('TagEditing', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts: [...contexts, { key: 'tags', value: writable({ data: [], isLoading: false }) }],
+ props: {
+ exo: baseExo
+ }
+ }
+ });
+ const expand = card.getByText('+', { exact: true });
+ fireEvent.click(expand).then(() => {
+ setTimeout(() => {
+ card.getByRole('button', { name: 'Annuler' });
+ card.getByRole('button', { name: 'Valider !' });
+ const tags = card.container.querySelector('.svelecte');
+ expect(tags).not.toBeNull();
+ }, 1000);
+ });
+ });
+
+ test('No auth', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts: [nav, notif, modal, alert, { key: 'auth', value: { isAuth: writable(false) } }],
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ expect(card.queryByTestId('copy')).toBeNull();
+ expect(card.queryByTestId('tags')).toBeNull();
+ expect(card.queryByTestId('delete')).toBeNull();
+ expect(card.queryByTestId('edit')).toBeNull();
+ card.getByText((c, e) => {
+ return c == 'Par' && e?.children[0].innerHTML == 'ouioui';
+ });
+ });
+ test('tags', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ card.getByTestId('tags');
+ });
+
+ test('Icons', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, is_author: true }
+ }
+ }
+ });
+
+ card.getByTestId('delete');
+ card.getByTestId('edit');
+ });
+
+ test('Example', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo }
+ }
+ }
+ });
+
+ const exs = card.queryAllByText(baseExo.examples.data[0].calcul);
+ expect(exs.length).toBe(3);
+ });
+
+ test('Check author', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, is_author: false }
+ }
+ }
+ });
+
+ card.getByText((c, e) => {
+ return c == 'Par' && e?.children[0].innerHTML == 'ouioui';
+ });
+ });
+ test('Check original author', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: {
+ ...baseExo,
+ is_author: true,
+ original: { id_code: 'AOB', name: 'test2', author: 'lilian' }
+ }
+ }
+ }
+ });
+
+ card.getByText((c, e) => {
+ return c == 'Exercice original de' && e?.children[0].innerHTML == 'lilian';
+ });
+ });
+
+ test('Check title', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: baseExo
+ }
+ }
+ });
+
+ card.getByText('Title');
+ });
+ test('Check consigne', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: baseExo
+ }
+ }
+ });
+
+ card.getByText('The consigne');
+ });
+
+ test('Check no consigne', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, consigne: null }
+ }
+ }
+ });
+ expect(card.queryByTestId('consigne')).toBeNull();
+ });
+
+ test('Public mark', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, private: false }
+ }
+ }
+ });
+
+ card.getByText('Public');
+ expect(card.queryByText('Privé')).toBeNull();
+ });
+
+ test('Private mark', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, private: true }
+ }
+ }
+ });
+ card.getByText('Privé');
+ expect(card.queryByText('Public')).toBeNull();
+ });
+ test('no csv', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, supports: { csv: false } }
+ }
+ }
+ });
+ const input = card.getByRole('textbox');
+ expect(input.disabled).toBeTruthy();
+ const btn = card.getByRole('button', { name: 'Télécharger' });
+ expect(btn.disabled).toBeTruthy();
+ });
+ test('csv', () => {
+ const card = render(ContextTest, {
+ props: {
+ component: DownloadForm,
+ contexts,
+ props: {
+ exo: { ...baseExo, supports: { csv: true } }
+ }
+ }
+ });
+ const input = card.getByRole('textbox');
+ expect(input.disabled).toBeFalsy();
+ const btn = card.getByRole('button', { name: 'Télécharger' });
+ expect(btn.disabled).toBeFalsy();
+ });
+});
+
+describe('Heading', () => {
+ const contexts = [auth, nav, modal];
+ test('Auth', () => {
+ const head = render(ContextTest, {
+ props: {
+ component: Head,
+ contexts: [
+ ...contexts,
+ { key: 'tags', value: writable({ data: [] }) },
+ { key: 'exos', value: writable({ data: { items: [] } }) }
+ ],
+ props: {
+ location: 'public'
+ }
+ }
+ });
+
+ head.getByRole('button', { name: 'Nouveau' });
+ const svelecte = head.container.querySelector('.svelecte');
+ expect(svelecte).not.toBeNull();
+ const select = head.getByRole('combobox');
+ expect(select.value).toBe('public');
+ });
+
+ test('No auth', () => {
+ const head = render(ContextTest, {
+ props: {
+ component: Head,
+ contexts: [
+ nav,
+ modal,
+ { key: 'auth', value: { isAuth: writable(false) } },
+ { key: 'tags', value: writable({ data: [] }) },
+ { key: 'exos', value: writable({ data: { items: [] } }) }
+ ],
+ props: {
+ location: 'public'
+ }
+ }
+ });
+
+ expect(head.queryByRole('button', { name: 'Nouveau' })).toBeNull();
+ const svelecte = head.container.querySelector('.svelecte');
+ expect(svelecte).toBeNull();
+ expect(head.queryByRole('combobox')).toBeNull();
+ });
+});
+
+describe('Tag container', () => {
+ test('Tag selection', () => {
+ const tags = [
+ { label: 'test1', color: '#00ff00', id_code: 'ABC' },
+ { label: 'test2', color: '#00ff00', id_code: 'DEF' },
+ { label: 'test3', color: '#00ff00', id_code: 'GHI' },
+ { label: 'test4', color: '#00ff00', id_code: 'KLM' }
+ ];
+ const selected = [tags[0], tags[2]];
+ const container = render(ContextTest, {
+ props: {
+ component: TagContainer,
+ contexts: [
+ notif,
+ {
+ key: 'tags',
+ value: writable({
+ data: tags
+ })
+ }
+ ],
+ props: {
+ exo: {
+ name: 'test',
+ id_code: 'ABD',
+ tags: selected
+ }
+ }
+ }
+ });
+
+ const expand = container.getByText('+', { exact: true });
+ fireEvent.click(expand).then(() => {
+ const tagsSelector = container.container.querySelector('.sv-control');
+ if (tagsSelector != null) {
+ fireEvent.click(tagsSelector).then(() => {
+ tags.map((t) => {
+ const tag = container.getByText(t.label);
+ console.log('HMMM', tag, tag.classList);
+ if (selected.includes(t)) {
+ expect(tag.classList.contains('disabled')).toBeTruthy();
+ } else {
+ expect(tag.classList.contains('disabled')).toBeFalsy();
+ }
+ });
+ });
+ }
+ });
+ });
+});
+
+describe('Tag', () => {
+ test('With delete', () => {
+ let called = false;
+ const remove = () => {
+ console.log('remove');
+ called = true;
+ };
+
+ const tag = render(ContextTest, {
+ props: {
+ component: Tag,
+ contexts: [notif],
+ props: {
+ label: 'test',
+ color: '#00ff00',
+ remove
+ }
+ }
+ });
+
+ tag.getByText('test');
+ const del = tag.getByRole('button');
+ expect(del).not.toBeNull();
+ fireEvent.click(del).then(() => {
+ expect(called).toBeTruthy();
+ });
+ });
+
+ test('Without delete', () => {
+ const tag = render(ContextTest, {
+ props: {
+ component: Tag,
+ contexts: [notif],
+ props: {
+ label: 'test',
+ color: '#00ff00'
+ }
+ }
+ });
+
+ tag.getByText('test');
+ expect(tag.queryByRole('button')).toBeNull();
+ });
+});
+
+describe('ExoForm', () => {
+ test('Edit', () => {
+ const exo = {
+ name: 'test',
+ id_code: 'ABC',
+ private: true,
+ consigne: 'the consigne',
+ exo_source: "test.py",
+ tags: []
+ };
+
+ const form = render(ContextTest, {
+ props: {
+ component: EditForm,
+ contexts: [notif, alert],
+ props: {
+ exo,
+ editing: true,
+ cancel: () => console.log('cancel'),
+ updateExo: () => console.log('update')
+ }
+ }
+ });
+
+
+ const input = form.getByRole('textbox', { name: 'Nom' });
+ expect(input.value).toBe(exo.name);
+ const consigneInput = form.getByRole('textbox', { name: 'Consigne' });
+ expect(consigneInput.value).toBe(exo.consigne);
+
+ form.getByText("test.py");
+
+ const privateInput = form.getByRole('checkbox', { name: 'Privé' });
+ expect(privateInput.checked).toBeTruthy();
+
+
+ const btn = form.getByRole('button', { name: 'Modifier' });
+ expect(btn.disabled).toBeFalsy();
+ const cancelBtn = form.getByRole('button', { name: 'Annuler' });
+ expect(cancelBtn.disabled).toBeFalsy();
+ });
+});
+
+
+describe("FileInput", () => {
+ test('with default value', () => {
+ const fileInput = render(FileInput, {
+ defaultValue: 'test.py',
+ id_code: 'ABC',
+ label: 'Choisir',
+ value: []
+ });
+
+ const input = fileInput.getByLabelText("Choisir");
+ fileInput.getByText("test.py");
+
+ fireEvent(input, createEvent("change", input, {
+ target: { files: [new File(["(⌐□_□)"], "test2.py", { type: "text/plain" })] }
+ })).then(() => {
+ fileInput.getByText('test2.py');
+ expect(fileInput.queryByText('test.py')).toBeNull();
+ });
+ })
+ test('whithout default value', () => {
+ const fileInput = render(FileInput, {
+ id_code: 'ABC',
+ label: 'Choisir',
+ value: []
+ });
+
+ const input = fileInput.getByLabelText("Choisir");
+ fileInput.getByText("...");
+
+ fireEvent(input, createEvent("change", input, {
+ target: { files: [new File(["(⌐□_□)"], "test2.py", { type: "text/plain" })] }
+ })).then(() => {
+ fileInput.getByText('test2.py');
+ expect(fileInput.queryByText('...')).toBeNull();
+ });
+ })
+})
+
+describe('Pagination', () => {
+ test('< 6', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 1,
+ total: 6
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ pagination.getByText('3');
+ pagination.getByText('4');
+ pagination.getByText('5');
+ pagination.getByText('6');
+ })
+ test('> 7 start', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 1,
+ total: 9
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ pagination.getByText('...');
+ pagination.getByText('8');
+ pagination.getByText('9');
+ })
+
+ test('> 7 middle', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 5,
+ total: 9
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ const trim = pagination.getAllByText('...');
+ expect(trim.length).toBe(2);
+ pagination.getByText('4');
+ pagination.getByText('5');
+ pagination.getByText('6');
+ pagination.getByText('8');
+ pagination.getByText('9');
+ })
+
+ test('> 7 end', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 8,
+ total: 9
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ pagination.getByText('...');
+ pagination.getByText('8');
+ pagination.getByText('9');
+ })
+ test('> 7 3', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 3,
+ total: 9
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ pagination.getByText('3');
+ pagination.getByText('4');
+ pagination.getByText('...');
+ pagination.getByText('8');
+ pagination.getByText('9');
+ })
+ test('> 7 4', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 4,
+ total: 9
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ pagination.getByText('3');
+ pagination.getByText('4');
+ pagination.getByText('5');
+ pagination.getByText('...');
+ pagination.getByText('8');
+ pagination.getByText('9');
+ })
+
+ test('> 7 3 before end', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 6,
+ total: 9
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ pagination.getByText('...');
+ pagination.getByText('5');
+ pagination.getByText('6');
+ pagination.getByText('7');
+ pagination.getByText('8');
+ pagination.getByText('9');
+ })
+
+ test('> 7 2 before end', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 7,
+ total: 9
+ }
+ });
+
+ pagination.getByText('1');
+ pagination.getByText('2');
+ pagination.getByText('...');
+ pagination.getByText('6');
+ pagination.getByText('7');
+ pagination.getByText('8');
+ pagination.getByText('9');
+ })
+
+
+ test('buttons enabled', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 2,
+ total: 9
+ }
+ });
+
+ const back = pagination.getByRole('button', { name: '<' });
+ expect(back.disabled).toBeFalsy();
+ const next = pagination.getByRole('button', { name: '>' });
+ expect(next.disabled).toBeFalsy();
+ })
+ test("next button disabled", () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 9,
+ total: 9
+ }
+ });
+
+ const next = pagination.getByRole('button', { name: '>' });
+ expect(next.disabled).toBeTruthy();
+ })
+
+ test('back button disabled', () => {
+ const pagination = render(Pagination, {
+ props: {
+ page: 1,
+ total: 9
+ }
+ });
+
+ const back = pagination.getByRole('button', { name: '<' });
+ expect(back.disabled).toBeTruthy();
+ })
+})
\ No newline at end of file
diff --git a/frontend/src/test/room.test.ts b/frontend/src/test/room.test.ts
new file mode 100644
index 0000000..338324e
--- /dev/null
+++ b/frontend/src/test/room.test.ts
@@ -0,0 +1,1137 @@
+import { render, fireEvent, createEvent } from '@testing-library/svelte';
+import { get, writable } from 'svelte/store';
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+import Members from '../components/rooms/Members.svelte';
+import ContextTest from './ContextTest.svelte';
+import InputChallenge from '../components/rooms/InputChallenge.svelte';
+import Stats from '../components/rooms/Stats.svelte';
+import html from 'svelte-htm';
+import Classement from 'src/components/rooms/Classement.svelte';
+import Challenge from 'src/components/rooms/Challenge.svelte';
+import ChallengesList from 'src/components/rooms/ChallengesList.svelte';
+import ExerciceSelector from 'src/components/exos/ExerciceSelector.svelte';
+import ExoList from 'src/components/exos/ExoList.svelte';
+import RoomHead from 'src/components/rooms/RoomHead.svelte';
+import ParcoursList from 'src/components/rooms/ParcoursList.svelte';
+const ws = { key: 'ws', value: { send: () => {} } };
+const m1 = {
+ username: 'test1',
+ reconnect_code: '',
+ isUser: true,
+ id_code: 'test',
+ isAdmin: true,
+ online: true,
+ clientId: 'test'
+};
+const m3 = {
+ username: 'test3',
+ reconnect_code: 'reconnect3',
+ isUser: false,
+ id_code: 'test3',
+ isAdmin: false,
+ online: true,
+ clientId: 'test3'
+};
+const m2 = {
+ username: 'test2',
+ reconnect_code: 'reconnect2',
+ isUser: false,
+ id_code: 'test2',
+ isAdmin: false,
+ online: false,
+ clientId: 'test2'
+};
+
+const w1 = { waiter_id: 'waiter1', username: 'waiter' };
+
+describe('Participants', () => {
+ test('Status', () => {
+ const room = render(ContextTest, {
+ props: {
+ component: Members,
+ contexts: [
+ {
+ key: 'room',
+ value: writable({
+ public: true,
+ members: [m1, m2, m3, w1]
+ })
+ },
+ {
+ key: 'member',
+ value: writable(m3)
+ },
+ ws
+ ],
+ props: {}
+ }
+ });
+
+ room.getByText('En ligne :');
+ room.getByText('Hors-ligne :');
+
+ const test1 = room.getByText('test1');
+ const test2 = room.getByText('test2');
+ const test3 = room.getByText('test3');
+
+ const admin = room.getByText('Administrateur');
+
+ expect(test1.classList.contains('online')).toBeTruthy();
+ expect(test1.classList.contains('offline')).toBeFalsy();
+ expect(test1.classList.contains('admin')).toBeTruthy();
+ expect(test1.classList.contains('member')).toBeFalsy();
+ expect(test1.contains(admin)).toBeTruthy();
+ expect(test1.classList.contains('bannable')).toBeFalsy();
+
+ expect(test2.classList.contains('offline')).toBeTruthy();
+ expect(test2.classList.contains('online')).toBeFalsy();
+ expect(test2.classList.contains('admin')).toBeFalsy();
+ expect(test2.classList.contains('member')).toBeFalsy();
+ expect(test2.classList.contains('bannable')).toBeFalsy();
+
+ expect(test3.classList.contains('online')).toBeTruthy();
+ expect(test3.classList.contains('offline')).toBeFalsy();
+ expect(test3.classList.contains('member')).toBeTruthy();
+ expect(test3.classList.contains('admin')).toBeFalsy();
+ expect(test3.classList.contains('bannable')).toBeFalsy();
+
+ expect(room.queryByText("Liste d'attente :")).toBeNull();
+ expect(room.queryByText('waiter')).toBeNull();
+
+ room.getByText('#reconnect3');
+ expect(room.queryByText('#reconnect2')).toBeNull();
+ expect(room.queryByText('#', { exact: true })).toBeNull();
+ });
+
+ test('No online', () => {
+ const room = render(ContextTest, {
+ props: {
+ component: Members,
+ contexts: [
+ {
+ key: 'room',
+ value: writable({
+ public: true,
+ members: [m1, m3]
+ })
+ },
+ {
+ key: 'member',
+ value: writable(m3)
+ },
+ ws
+ ],
+ props: {}
+ }
+ });
+
+ expect(room.queryByText('Hors-ligne :')).toBeNull();
+ });
+
+ test('Admin', () => {
+ const room = render(ContextTest, {
+ props: {
+ component: Members,
+ contexts: [
+ {
+ key: 'room',
+ value: writable({
+ public: true,
+ members: [m1, m2, m3, w1]
+ })
+ },
+ {
+ key: 'member',
+ value: writable(m1)
+ },
+ ws
+ ],
+ props: {}
+ }
+ });
+
+ const test1 = room.getByText('test1');
+ const test2 = room.getByText('test2');
+ const test3 = room.getByText('test3');
+
+ expect(test1.classList.contains('bannable')).toBeFalsy();
+ expect(test2.classList.contains('bannable')).toBeTruthy();
+ expect(test3.classList.contains('bannable')).toBeTruthy();
+
+ room.getByText('#reconnect2');
+ room.getByText('#reconnect3');
+ room.getByTestId('visibilityChange');
+
+ room.getByText("Liste d'attente :");
+ const waiter1 = room.getByText('waiter');
+ room.getByText('#waiter1');
+ expect(waiter1.querySelector('.accept')).not.toBeNull();
+ expect(waiter1.querySelector('.refuse')).not.toBeNull();
+ });
+
+ test('not admin', () => {
+ const room = render(ContextTest, {
+ props: {
+ component: Members,
+ contexts: [
+ {
+ key: 'room',
+ value: writable({
+ public: true,
+ members: [m1, m2, m3, w1]
+ })
+ },
+ {
+ key: 'member',
+ value: writable(m3)
+ },
+ ws
+ ],
+ props: {}
+ }
+ });
+
+ const test1 = room.getByText('test1');
+ const test2 = room.getByText('test2');
+ const test3 = room.getByText('test3');
+
+ expect(test1.classList.contains('bannable')).toBeFalsy();
+ expect(test2.classList.contains('bannable')).toBeFalsy();
+ expect(test3.classList.contains('bannable')).toBeFalsy();
+
+ expect(room.queryByText('#reconnect2')).toBeNull();
+
+ room.getByText('#reconnect3');
+ expect(room.queryByTestId('visibilityChange')).toBeNull();
+
+ expect(room.queryByText("Liste d'attente :")).toBeNull();
+ expect(room.queryByText('waiter')).toBeNull();
+ });
+
+ test('ChangeVisibility', async () => {
+ const roomStore = writable({
+ public: true,
+ members: [m1, m2, m3, w1]
+ });
+
+ const room = render(ContextTest, {
+ props: {
+ component: Members,
+ contexts: [
+ {
+ key: 'room',
+ value: roomStore
+ },
+ {
+ key: 'member',
+ value: writable(m1)
+ },
+ {
+ key: 'ws',
+ value: {
+ send: (t, d) => {
+ console.log('update', d.public);
+ if (t === 'set_visibility') {
+ roomStore.update((r) => {
+ r.public = d.public;
+ return r;
+ });
+ }
+ }
+ }
+ }
+ ],
+ props: {}
+ }
+ });
+
+ room.getByText('#reconnect2');
+ room.getByText('#reconnect3');
+ const visibilityChange = room.getByTestId('visibilityChange');
+ room.getByTestId('public');
+ expect(room.queryByTestId('private')).toBeNull();
+ expect(visibilityChange.classList.contains('public')).toBeTruthy();
+ expect(visibilityChange.classList.contains('private')).toBeFalsy();
+
+ await fireEvent.click(visibilityChange);
+ room.getByTestId('private');
+ expect(room.queryByTestId('public')).toBeNull();
+ expect(visibilityChange.classList.contains('public')).toBeFalsy();
+ expect(visibilityChange.classList.contains('private')).toBeTruthy();
+
+ await fireEvent.click(visibilityChange);
+ room.getByTestId('public');
+ expect(room.queryByTestId('private')).toBeNull();
+ expect(visibilityChange.classList.contains('public')).toBeTruthy();
+ expect(visibilityChange.classList.contains('private')).toBeFalsy();
+ });
+});
+
+describe('Input challenge', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+ test('Challenging', async () => {
+ const input = render(InputChallenge, {
+ props: {
+ correction: null,
+ value: '',
+ corrigeable: false,
+ corriged: false
+ }
+ });
+
+ const inputElement = input.container.querySelector('span.input');
+
+ const hiddenInput = input.getByTestId('hiddenInput');
+
+ expect(inputElement?.getAttribute('contenteditable')).toBe('true');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeTruthy();
+ expect(hiddenInput.classList.contains('close')).toBeTruthy();
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('none');
+ await fireEvent.click(inputElement);
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('none');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeTruthy();
+ expect(hiddenInput.classList.contains('close')).toBeTruthy();
+ });
+
+ test('Open', async () => {
+ const input = render(InputChallenge, {
+ props: {
+ correction: 'test',
+ value: 'test',
+ corrigeable: true,
+ corriged: true,
+ valid: true
+ }
+ });
+
+ const inputElement = input.container.querySelector('span.input');
+
+ const hiddenInput = input.getByTestId('hiddenInput');
+
+ expect(inputElement?.getAttribute('contenteditable')).toBe('false');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeTruthy();
+ expect(hiddenInput.classList.contains('close')).toBeTruthy();
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('none');
+ await fireEvent.click(inputElement);
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('flex');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeFalsy();
+ expect(hiddenInput.classList.contains('close')).toBeFalsy();
+
+ const corrInput = input.container.querySelector('input');
+ expect(corrInput.value).toBe('test');
+ expect(document.activeElement).toBe(corrInput);
+ });
+
+ test('close', async () => {
+ const input = render(InputChallenge, {
+ props: {
+ correction: 'test',
+ value: 'test',
+ corrigeable: true,
+ corriged: true,
+ valid: true
+ }
+ });
+
+ const inputElement = input.container.querySelector('span.input');
+
+ const hiddenInput = input.getByTestId('hiddenInput');
+
+ expect(inputElement?.getAttribute('contenteditable')).toBe('false');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeTruthy();
+ expect(hiddenInput.classList.contains('close')).toBeTruthy();
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('none');
+
+ await fireEvent.click(inputElement);
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('flex');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeFalsy();
+ expect(hiddenInput.classList.contains('close')).toBeFalsy();
+
+ const test = document.createElement('div');
+ document.body.appendChild(test);
+ //await fireEvent.focusOut(hiddenInput, { relatedTarget: test });
+ await fireEvent.click(inputElement);
+ setTimeout(async () => {
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('none');
+ expect(hiddenInput.classList.contains('hidden')).toBeTruthy();
+ expect(hiddenInput.classList.contains('close')).toBeTruthy();
+
+ await fireEvent.click(inputElement);
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('flex');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeFalsy();
+ expect(hiddenInput.classList.contains('close')).toBeFalsy();
+
+ await fireEvent.click(document.body);
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('none');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeTruthy();
+ expect(hiddenInput.classList.contains('close')).toBeTruthy();
+ }, 400);
+ });
+
+ test('Corriging', async () => {
+ const correction = writable('test');
+ const valid = writable(true);
+ const input = render(
+ html`<${InputChallenge}
+ bind:correction=${correction}
+ value="test"
+ bind:valid=${valid}
+ corrigeable=${true}
+ corriged=${true}
+ />`
+ );
+
+ const inputElement = input.container.querySelector('span.input');
+
+ await fireEvent.click(inputElement);
+
+ const corrInput = input.container.querySelector('input');
+ expect(corrInput.value).toBe('test');
+
+ await fireEvent.input(corrInput, { target: { value: 'test2' } });
+ expect(get(correction)).toBe('test2');
+ expect(get(valid)).toBeFalsy();
+
+ await fireEvent.input(corrInput, { target: { value: 'test' } });
+ expect(get(correction)).toBe('test');
+ expect(get(valid)).toBeTruthy();
+ });
+
+ test('Corrige from null correction', async () => {
+ const correction = writable(null);
+ const valid = writable(false);
+ const input = render(
+ html`<${InputChallenge}
+ bind:correction=${correction}
+ value="test"
+ bind:valid=${valid}
+ corrigeable=${true}
+ corriged=${true}
+ />`
+ );
+
+ const inputElement = input.container.querySelector('span.input');
+
+ await fireEvent.click(inputElement);
+ const corrInput = input.container.querySelector('input');
+ expect(corrInput.value).toBe('');
+
+ await fireEvent.input(corrInput, { target: { value: 'test2' } });
+ expect(get(correction)).toBe('test2');
+ expect(get(valid)).toBeFalsy();
+
+ await fireEvent.input(corrInput, { target: { value: 'test' } });
+ expect(get(correction)).toBe('test');
+ expect(get(valid)).toBeTruthy();
+ });
+
+ test('Change valid', async () => {
+ const correction = writable(null);
+ const valid = writable(false);
+ const input = render(
+ html`<${InputChallenge}
+ bind:correction=${correction}
+ value="test"
+ bind:valid=${valid}
+ corrigeable=${true}
+ corriged=${true}
+ />`
+ );
+
+ const inputElement = input.container.querySelector('span.input');
+
+ await fireEvent.click(inputElement);
+ const validToggle = input.getByTestId('valid');
+ const invalid = input.getByTestId('invalid');
+ console.log(invalid);
+
+ await fireEvent.mouseDown(validToggle);
+ expect(get(valid)).toBeTruthy();
+
+ await fireEvent.mouseDown(invalid);
+ expect(get(valid)).toBeFalsy();
+
+ await fireEvent.mouseDown(validToggle);
+ expect(get(valid)).toBeTruthy();
+
+ const corrInput = input.container.querySelector('input');
+ await fireEvent.input(corrInput, { target: { value: 'test2' } });
+ expect(get(valid)).toBeFalsy();
+
+ await fireEvent.mouseDown(validToggle);
+ expect(get(valid)).toBeTruthy();
+ });
+
+ test('Corriged not corrigeable', async () => {
+ const input = render(InputChallenge, {
+ props: {
+ correction: 'test',
+ value: 'test',
+ corrigeable: false,
+ corriged: true
+ }
+ });
+
+ const inputElement = input.container.querySelector('span.input');
+ const hiddenInput = input.getByTestId('hiddenInput');
+ expect(inputElement?.getAttribute('contenteditable')).toBe('false');
+ await fireEvent.click(inputElement);
+
+ expect(window.getComputedStyle(hiddenInput.children[0] as Element).display).toBe('none');
+
+ expect(hiddenInput.classList.contains('hidden')).toBeTruthy();
+ expect(hiddenInput.classList.contains('close')).toBeTruthy();
+ });
+});
+
+const createChallenge = (note, time, id) => {
+ return {
+ id_code: id,
+ mistakes: note,
+ time: time,
+ isCorriged: true,
+ canCorriged: true,
+ validated: true
+ };
+};
+describe('Stats', () => {
+ test('Validated', () => {
+ const stats = render(ContextTest, {
+ props: {
+ component: Stats,
+ contexts: [
+ {
+ key: 'parcours',
+ value: writable({
+ validated: true,
+ memberRank: null,
+ ranking: [],
+ rank: null,
+ tops: [],
+ pb: null,
+ challenges: {}
+ })
+ },
+ { key: 'member', value: writable(m1) }
+ ],
+ props: {}
+ }
+ });
+
+ stats.getByText('Parcours validé !');
+ });
+ test('Not validated', () => {
+ const stats = render(ContextTest, {
+ props: {
+ component: Stats,
+ contexts: [
+ {
+ key: 'parcours',
+ value: writable({
+ validated: false,
+ memberRank: null,
+ ranking: [],
+ rank: null,
+ tops: [],
+ pb: null,
+ challenges: {}
+ })
+ },
+ { key: 'member', value: writable(m1) }
+ ],
+ props: {}
+ }
+ });
+ stats.getByText('Parcours non validé !');
+ });
+
+ test('Stats', () => {
+ const stats = render(ContextTest, {
+ props: {
+ component: Stats,
+ contexts: [
+ {
+ key: 'parcours',
+ value: writable({
+ validated: true,
+ memberRank: 1,
+ ranking: [],
+ rank: 1,
+ tops: [],
+ pb: null,
+ challenges: {
+ [m1.id_code]: {
+ challenger: { username: 'test', id_code: m1.id_code },
+ challenges: [
+ createChallenge(1, 10, '1'),
+ createChallenge(2, 10, '1'),
+ createChallenge(3, 10, '1'),
+ createChallenge(4, 10, '1'),
+ createChallenge(5, 10, '1'),
+ createChallenge(6, 10, '1')
+ ]
+ }
+ }
+ })
+ },
+ { key: 'member', value: writable(m1) }
+ ],
+ props: {}
+ }
+ });
+
+ const avg = stats.getByTestId('avg');
+ expect(avg.textContent).toContain('3.5');
+ const best = stats.getByTestId('max');
+ expect(best.textContent).toContain('6');
+ const worst = stats.getByTestId('min');
+ expect(worst.textContent).toContain('1');
+ });
+
+ test('Stats no challenges', () => {
+ const stats = render(ContextTest, {
+ props: {
+ component: Stats,
+ contexts: [
+ {
+ key: 'parcours',
+ value: writable({
+ validated: true,
+ memberRank: 1,
+ ranking: [],
+ rank: 1,
+ tops: [],
+ pb: null,
+ challenges: {}
+ })
+ },
+ { key: 'member', value: writable(m1) }
+ ],
+ props: {}
+ }
+ });
+ const avg = stats.getByTestId('avg');
+ expect(avg.textContent).toContain('-');
+ const best = stats.getByTestId('max');
+ expect(best.textContent).toContain('-');
+ const worst = stats.getByTestId('min');
+ expect(worst.textContent).toContain('-');
+ });
+});
+
+describe('Ranking', () => {
+ test('No rank', () => {
+ const ranking = render(Classement, {
+ props: {
+ tops: [
+ { name: 'test1', value: 'Moyenne 3.5' },
+ { name: 'test2', value: 'Moyenne 6.5' },
+ { name: 'test3', value: 'Moyenne 5.5' }
+ ]
+ }
+ });
+
+ ranking.getByText((c, e) => {
+ return e.textContent == '#1 - test1 - Moyenne 3.5';
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#2 - test2 - Moyenne 6.5';
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#3 - test3 - Moyenne 5.5';
+ });
+ });
+
+ test('Top > 3', () => {
+ const ranking = render(Classement, {
+ props: {
+ tops: [
+ { name: 'test1', value: 'Moyenne 3.5' },
+ { name: 'test2', value: 'Moyenne 6.5' },
+ { name: 'test3', value: 'Moyenne 5.5' }
+ ],
+ rank: { rank: 10, name: 'test4', value: 'Moyenne 120' }
+ }
+ });
+
+ ranking.getByText((c, e) => {
+ return e.textContent == '#1 - test1 - Moyenne 3.5';
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#2 - test2 - Moyenne 6.5';
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#3 - test3 - Moyenne 5.5';
+ });
+
+ ranking.getByText((c, e) => {
+ return e.textContent == '#10 - test4 - Moyenne 120';
+ });
+ });
+
+ test('Top < 3', () => {
+ const ranking = render(Classement, {
+ props: {
+ tops: [
+ { name: 'test1', value: 'Moyenne 3.5' },
+ { name: 'test2', value: 'Moyenne 6.5' },
+ { name: 'test3', value: 'Moyenne 5.5' }
+ ],
+ rank: { rank: 1, name: 'test1', value: 'Moyenne 3.5' }
+ }
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#1 - test1 - Moyenne 3.5';
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#2 - test2 - Moyenne 6.5';
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#3 - test3 - Moyenne 5.5';
+ });
+ });
+
+ test('Incomplete', () => {
+ const ranking = render(Classement, {
+ props: {
+ tops: [
+ { name: 'test1', value: 'Moyenne 3.5' },
+ { name: 'test2', value: 'Moyenne 6.5' }
+ ]
+ }
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#1 - test1 - Moyenne 3.5';
+ });
+ ranking.getByText((c, e) => {
+ return e.textContent == '#2 - test2 - Moyenne 6.5';
+ });
+ ranking.getByText('#3 -');
+ });
+});
+
+describe('Challenges', () => {
+ test('No challenges', () => {
+ const challenges = render(ContextTest, {
+ props: {
+ component: ChallengesList,
+ contexts: [
+ {
+ key: 'parcours',
+ value: writable({
+ challenges: {}
+ })
+ },
+ { key: 'member', value: writable(m1) }
+ ],
+ props: {}
+ }
+ });
+ challenges.getByText('Aucun essai effectué :(');
+ });
+
+ test('Only self', async () => {
+ const challenges = render(ContextTest, {
+ props: {
+ component: ChallengesList,
+ contexts: [
+ {
+ key: 'parcours',
+ value: writable({
+ challenges: {
+ test: {
+ challenger: { username: 'test', id_code: 'test' },
+ challenges: [createChallenge(6, 10, '1')]
+ }
+ }
+ })
+ },
+ { key: 'member', value: writable(m1) }
+ ],
+ props: {}
+ }
+ });
+ const extend = challenges.getByText('Vos essais');
+ await fireEvent.click(extend);
+
+ challenges.getByText((t, e) => {
+ console.log('PH', t, e?.textContent);
+ return e.textContent == '6 fautes en 10s';
+ });
+ });
+
+ test('Others', async () => {
+ const challenges = render(ContextTest, {
+ props: {
+ component: ChallengesList,
+ contexts: [
+ {
+ key: 'parcours',
+ value: writable({
+ challenges: {
+ test: {
+ challenger: { name: 'test', id_code: 'test' },
+ challenges: [createChallenge(1, 10, '1')]
+ },
+ test2: {
+ challenger: { name: 'test2', id_code: 'test2' },
+ challenges: [createChallenge(10, 70, '1')]
+ }
+ }
+ })
+ },
+ { key: 'member', value: writable(m3) }
+ ],
+ props: {}
+ }
+ });
+ const extendTest1 = challenges.getByText('test');
+ await fireEvent.click(extendTest1);
+
+ challenges.getByText((t, e) => {
+ return e.textContent == '1 faute en 10s';
+ });
+
+ const extendText2 = challenges.getByText('test2');
+ await fireEvent.click(extendText2);
+ challenges.getByText((t, e) => {
+ console.log('PH', t, e?.textContent);
+ return e.textContent == '10 fautes en 01 : 10s';
+ });
+ expect(
+ challenges.queryByText((t, e) => {
+ return e.textContent == '1 faute en 10s';
+ })
+ ).toBeNull();
+
+ await fireEvent.click(extendText2);
+ expect(
+ challenges.queryByText((t, e) => {
+ return e.textContent == '1 faute en 10s';
+ })
+ ).toBeNull();
+ expect(
+ challenges.queryByText((t, e) => {
+ return e.textContent == '10 fautes en 01 : 10s';
+ })
+ ).toBeNull();
+ });
+});
+
+describe('Exo selector', () => {
+ test('No exo', () => {
+ const exoSelector = render(ContextTest, {
+ props: {
+ component: ExerciceSelector,
+ contexts: [{ key: 'modal', value: { show: () => {} } }],
+ props: { exos: writable([]) }
+ }
+ });
+ exoSelector.getByText('Aucun exercice sélectionné');
+ });
+
+ test('Exo', async () => {
+ const exoSelector = render(ContextTest, {
+ props: {
+ component: ExerciceSelector,
+ contexts: [{ key: 'modal', value: { show: () => {} } }],
+ props: { exos: writable([{ quantity: 1, name: 'test', exercice_id: '10' }]) }
+ }
+ });
+ exoSelector.getByText('test');
+ exoSelector.getByRole('spinbutton', { name: 'Nombre' });
+ });
+
+ test('open modal', async () => {
+ const contexts = {
+ show: (c, p, b) => {
+ expect(c.toString()).toBe(ExoList.toString());
+ }
+ };
+
+ const show = vi.spyOn(contexts, 'show');
+ const exoSelector = render(ContextTest, {
+ props: {
+ component: ExerciceSelector,
+ contexts: [{ key: 'modal', value: { show } }],
+ props: { exos: writable([]) }
+ }
+ });
+ const addButton = exoSelector.getByRole('button', { name: '+' });
+ await fireEvent.click(addButton);
+ expect(show).toHaveBeenCalled();
+ });
+});
+
+describe('Exo list', () => {
+ test('No auth', () => {
+ const exoList = render(ContextTest, {
+ props: {
+ component: ExoList,
+ contexts: [{ key: 'auth', value: { isAuth: writable(false) } }],
+ props: { exos: writable([]) }
+ }
+ });
+ exoList.getByPlaceholderText('Rechercher...');
+ expect(exoList.queryByPlaceholderText('Selectionner')).toBeNull();
+ expect(exoList.queryByRole('option', { name: 'Tous les exercices' })).toBeNull();
+ expect(exoList.queryByRole('option', { name: 'Vos exercices' })).toBeNull();
+ });
+ test('Auth', () => {
+ const exoList = render(ContextTest, {
+ props: {
+ component: ExoList,
+ contexts: [{ key: 'auth', value: { isAuth: writable(true) } }],
+ props: { exos: writable([]) }
+ }
+ });
+ exoList.getByPlaceholderText('Rechercher...');
+ exoList.getByRole('option', { name: 'Tous les exercices' });
+ exoList.getByRole('option', { name: 'Vos exercices' });
+ });
+});
+
+describe('Room Head', () => {
+ test('Input', async () => {
+ const room = writable({
+ name: 'testRoom',
+ id_code: 'Ouioui'
+ });
+ const contexts = {
+ send: (t, d) => {
+ expect(d.name).toBe('testRoom2');
+ if (t == 'set_name') {
+ room.update((r) => {
+ r.name = d.name;
+ return r;
+ });
+ }
+ }
+ };
+ const send = vi.spyOn(contexts, 'send');
+
+ const roomHead = render(ContextTest, {
+ props: {
+ component: RoomHead,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {
+ key: 'room',
+ value: room
+ },
+ { key: 'ws', value: { send, ws: {} } }
+ ],
+ props: {}
+ }
+ });
+
+ let name = roomHead.getByText('testRoom', { exact: true });
+ const id_code = roomHead.getByText('#Ouioui');
+ const input = roomHead.getByRole('textbox');
+
+ expect(input.classList.contains('hide')).toBeTruthy();
+ await fireEvent.dblClick(name);
+ expect(input.value).toBe('testRoom');
+ expect(input.classList.contains('hide')).toBeFalsy();
+ expect(document.activeElement).toBe(input);
+ expect(roomHead.queryByText('testRoom')).toBeNull();
+
+ await fireEvent.keyDown(input, { key: 'Escape', code: 'Escape' });
+ expect(input.classList.contains('hide')).toBeTruthy();
+ name = roomHead.getByText('testRoom', { exact: true });
+ await fireEvent.dblClick(name);
+ expect(input.classList.contains('hide')).toBeFalsy();
+ expect(document.activeElement).toBe(input);
+ expect(roomHead.queryByText('testRoom')).toBeNull();
+
+ await fireEvent.input(input, { target: { value: 'testRoom2' } });
+ await fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
+ name = roomHead.getByText('testRoom2', { exact: true });
+ expect(send).toHaveBeenCalled();
+ expect(input.classList.contains('hide')).toBeTruthy();
+
+ await fireEvent.dblClick(id_code, {});
+ expect(input.classList.contains('hide')).toBeTruthy();
+ });
+
+ test('Admin', () => {
+ const roomHead = render(ContextTest, {
+ props: {
+ component: RoomHead,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {
+ key: 'room',
+ value: writable({
+ name: 'testRoom',
+ id_code: 'Ouioui'
+ })
+ },
+ { key: 'ws', value: { send: () => {}, ws: {} } }
+ ],
+ props: {}
+ }
+ });
+ roomHead.getByTestId('delete');
+ roomHead.getByTestId('refresh');
+ roomHead.getByTestId('leave');
+ });
+ test('Not admin', async () => {
+ const roomHead = render(ContextTest, {
+ props: {
+ component: RoomHead,
+ contexts: [
+ { key: 'member', value: writable(m2) },
+ {
+ key: 'room',
+ value: writable({
+ name: 'testRoom',
+ id_code: 'Ouioui'
+ })
+ },
+ { key: 'ws', value: { send: () => {}, ws: {} } }
+ ],
+ props: {}
+ }
+ });
+ expect(roomHead.queryByTestId('delete')).toBeNull();
+ roomHead.getByTestId('refresh');
+ roomHead.getByTestId('leave');
+
+ const name = roomHead.getByText('testRoom', { exact: true });
+ const input = roomHead.getByRole('textbox');
+
+ expect(input.classList.contains('hide')).toBeTruthy();
+ await fireEvent.dblClick(name);
+ expect(input.classList.contains('hide')).toBeTruthy();
+ });
+});
+
+describe('ParcoursList', () => {
+ test('Admin', () => {
+ const parcoursList = render(ContextTest, {
+ props: {
+ component: ParcoursList,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {key: "room", value: writable({id_code: "oui", parcours: [{name: "testParcours", best_note: null, id_code: "yes", validated: false}]})},
+ { key: 'alert', value: { alert: () => {}} }
+ ],
+ props: { parcours: writable([]) }
+ }
+ });
+ parcoursList.getByRole('button', { name: 'Nouveau' });
+ const parcours = parcoursList.getByText('testParcours');
+ expect(parcours.parentElement?.querySelector('.delete')).not.toBeNull();
+
+ });
+ test('Not admin', () => {
+ const parcoursList = render(ContextTest, {
+ props: {
+ component: ParcoursList,
+ contexts: [
+ { key: 'member', value: writable(m2) },
+ {
+ key: 'room',
+ value: writable({
+ id_code: 'oui',
+ parcours: [
+ { name: 'testParcours', best_note: null, id_code: 'yes', validated: false }
+ ]
+ })
+ },
+ { key: 'alert', value: { alert: () => {} } }
+ ],
+ props: { parcours: writable([]) }
+ }
+ });
+ expect(parcoursList.queryByRole('button', { name: 'Nouveau' })).toBeNull();
+ const parcours = parcoursList.getByText('testParcours');
+ expect(parcours.parentElement?.querySelector('.delete')).toBeNull();
+ })
+ test('Empty', () => {
+ const parcoursList = render(ContextTest, {
+ props: {
+ component: ParcoursList,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {key: "room", value: writable({id_code: "oui", parcours: []})},
+ { key: 'alert', value: { alert: () => {}} }
+ ],
+ props: { parcours: writable([]) }
+ }
+ });
+ parcoursList.getByText('Aucun parcours pour le moment');
+ })
+
+ test('With best note', () => {
+ const parcoursList = render(ContextTest, {
+ props: {
+ component: ParcoursList,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {key: "room", value: writable({id_code: "oui", parcours: [{name: "testParcours", best_note: 12, id_code: "yes", validated: false}]})},
+ { key: 'alert', value: { alert: () => {}} }
+ ],
+ props: { parcours: writable([]) }
+ }
+ });
+ parcoursList.getByText('Record : 12 fautes');
+ })
+ test('Without best note', () => {
+ const parcoursList = render(ContextTest, {
+ props: {
+ component: ParcoursList,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {key: "room", value: writable({id_code: "oui", parcours: [{name: "testParcours", best_note: null, id_code: "yes", validated: false}]})},
+ { key: 'alert', value: { alert: () => {}} }
+ ],
+ props: { parcours: writable([]) }
+ }
+ });
+ parcoursList.getByText('Aucun essai effectué');
+ })
+ test('Validated', () => {
+ const parcoursList = render(ContextTest, {
+ props: {
+ component: ParcoursList,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {key: "room", value: writable({id_code: "oui", parcours: [{name: "testParcours", best_note: 12, id_code: "yes", validated: true}]})},
+ { key: 'alert', value: { alert: () => {}} }
+ ],
+ props: { parcours: writable([]) }
+ }
+ });
+ parcoursList.getByTestId('valid');
+ })
+ test('Not validated', () => {
+ const parcoursList = render(ContextTest, {
+ props: {
+ component: ParcoursList,
+ contexts: [
+ { key: 'member', value: writable(m1) },
+ {key: "room", value: writable({id_code: "oui", parcours: [{name: "testParcours", best_note: 12, id_code: "yes", validated: false}]})},
+ { key: 'alert', value: { alert: () => {}} }
+ ],
+ props: { parcours: writable([]) }
+ }
+ });
+ expect(parcoursList.queryByTestId('valid')).toBeNull();
+ })
+});
+
diff --git a/frontend/src/types/auth.type.ts b/frontend/src/types/auth.type.ts
new file mode 100644
index 0000000..875e3fc
--- /dev/null
+++ b/frontend/src/types/auth.type.ts
@@ -0,0 +1,14 @@
+export type User = {
+ username: string
+ firstname: string | null
+ name: string | null
+ email: string | null
+ id: number
+ rooms: UsersRoom[]
+}
+
+export type UsersRoom = {
+ name: string
+ id_code: string
+ admin: boolean
+}
\ No newline at end of file
diff --git a/frontend/src/types/exo.type.ts b/frontend/src/types/exo.type.ts
index 6bc69b1..2987e44 100644
--- a/frontend/src/types/exo.type.ts
+++ b/frontend/src/types/exo.type.ts
@@ -6,9 +6,7 @@ export type Tag = {
export type Exercice = {
- csv: boolean,
- pdf: boolean,
- web: boolean,
+ supports: {csv: boolean, pdf:boolean, web:boolean}
name: string,
consigne: string,
private: boolean,
diff --git a/frontend/src/types/room.type.ts b/frontend/src/types/room.type.ts
new file mode 100644
index 0000000..6312ec3
--- /dev/null
+++ b/frontend/src/types/room.type.ts
@@ -0,0 +1,103 @@
+
+export type Room = {
+ public: boolean;
+ global_results: boolean;
+ name: string,
+ id_code: string,
+ members: (Member | Waiter)[]
+ parcours: Parcours[]
+};
+
+export type Parcours = {
+ name: string,
+ best_note: number,
+ id_code: string,
+ validated: boolean
+}
+
+export type Member = {
+ username: string,
+ reconnect_code: string,
+ isUser: boolean,
+ id_code: string,
+ isAdmin: string,
+ online: boolean,
+ clientId: string
+}
+
+export type Waiter = {
+ username: string,
+ waiter_id: string
+}
+
+export type ExoInfos = {
+ name: string,
+ consigne: string,
+ id_code: string
+}
+export type ParcoursInfos = {
+ name: string,
+ id_code: string,
+ time: number,
+ max_mistakes: number
+}
+
+
+
+export type Input = {
+ value: string;
+ index: number;
+ correction?: string | null;
+ valid?: boolean | null;
+};
+
+export type Calcul = {
+ calcul: string,
+ inputs: Input[]
+}
+
+export type Challenge = {
+ exo: ExoInfos,
+ data: Calcul[]
+}
+
+export type Note = {
+ value: number,
+ total: number,
+ temporary: boolean
+}
+
+
+export type ExoSelect = {
+ quantity: number | string,
+ exercice_id: string,
+ name: string,
+}
+
+export type ChallengeInfo = {
+ challenger: { name: string, id_code: string },
+ challenges: {id_code: string, mistakes: number, time: number, isCorriged: boolean, canCorrige: boolean, validated: boolean}[]
+}
+
+export type Exercices = {
+ exercice_id: string,
+ quantity: number,
+ name: string;
+ examples: { type: string; data: { calcul: string; correction: string }[] };
+};
+
+export type ParcoursRead = {
+ name: string,
+ time: number,
+ max_mistakes: number,
+ id_code: string,
+ rank: number | null,
+ memberRank: number | null,
+ validated: boolean,
+ challenges: { [id: string]: ChallengeInfo }
+ exercices: Exercices[],
+ ranking: { name: string, id_code: string, avg: number }[],
+ tops: { challenger: { name: string, id_code: string }, mistakes: number, time: number }[],
+ pb: { mistakes: number, time: number },
+ avg: number | null
+}
\ No newline at end of file
diff --git a/frontend/src/utils/forms.ts b/frontend/src/utils/forms.ts
index 83e5918..cc4908f 100644
--- a/frontend/src/utils/forms.ts
+++ b/frontend/src/utils/forms.ts
@@ -7,14 +7,16 @@ export const errorMsg = (
},
name: string
): string[] => {
+ console.log(form.errors)
return [
form.hasError(`${name}.required`) && 'Champ requis',
form.hasError(`${name}.min`) && 'Trop court',
form.hasError(`${name}.max`) && 'Trop long',
form.hasError(`${name}.url`) && 'Pas bonne url',
form.hasError(`${name}.between`) && 'Pas valide',
- form.hasError(`${name}.matchField`) && 'Ca matche pas',
+ form.hasError(`${name}.match_field`) && 'Les champs ne correspondent pas',
form.hasError(`${name}.not`) && 'Valeur impossible',
+ form.hasError(`${name}.pattern`) && 'Un chiffre et une majuscule obligatoire',
].filter((r) => typeof r === 'string') as string[];
};
diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts
index 0cf4fb0..0cacf13 100644
--- a/frontend/src/utils/utils.ts
+++ b/frontend/src/utils/utils.ts
@@ -1,16 +1,61 @@
+import { browser } from "$app/environment";
+import type { AxiosHeaders, AxiosRequestConfig } from "axios";
+import jwtDecode from "jwt-decode";
+import { refreshRequest } from "../requests/auth.request";
+
export const compareObject = (obj1: object, obj2: object) => {
const keys = Object.keys(obj1);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
- const v1 = obj1[key as keyof typeof obj1];
- const v2 = obj2[key as keyof typeof obj2];
- console.log(obj1, obj2, v1, v2, key)
- if (v1 != undefined && v2 != undefined) {
- if (v1 != v2) {
- return false
- }
- }
-
+ const v1 = obj1[key as keyof typeof obj1];
+ const v2 = obj2[key as keyof typeof obj2];
+ console.log(obj1, obj2, v1, v2, key);
+ if (v1 != undefined && v2 != undefined) {
+ if (v1 != v2) {
+ return false;
+ }
+ }
}
return true;
};
+
+export const checkExpire = (exp: number) => {
+ return Date.now() >= exp * 1000;
+};
+
+export const parseTimer = (tps: number) => {
+ const remainder = Math.abs(tps) / 60;
+ const min = Math.floor(remainder);
+ const sec = Math.abs(tps) - 60 * min;
+
+ return `${tps < 0 ? "+ ": ""}${String(min).length == 1 && min > 0 ? '0' : ''}${min > 0 ? String(min) + " : ": ""}${
+ String(sec).length == 1 ? '0' : ''
+ }${sec}s`;
+};
+
+
+
+export const autoRefresh = async (config: AxiosRequestConfig) => {
+ if (!browser || config.url?.includes('/refresh')) return config
+ let access = localStorage.getItem('token');
+ const refresh = localStorage.getItem('refresh');
+ if(access == null) return config
+ const { exp } = jwtDecode<{exp: number}>(access);
+
+ if (refresh != null && checkExpire(exp)) {
+ access = await refreshRequest(refresh).then((r) => {
+ localStorage.setItem('token', r.access_token);
+ return r.access_token
+ }).catch(()=>access)
+ }
+ (config.headers as AxiosHeaders).set("Authorization", `Bearer ${access}`)
+ return config;
+};
+
+export const average = (arr: Array) => {
+ return arr.reduce((partialSum, a) => partialSum + a, 0) / arr.length;
+};
+
+export const statsCalculator = (arr: Array) => {
+ return { avg: average(arr), min: Math.min(...arr), max: Math.max(...arr) };
+};
\ No newline at end of file
diff --git a/frontend/src/variables.scss b/frontend/src/variables.scss
index ec9ea78..6bec8b0 100644
--- a/frontend/src/variables.scss
+++ b/frontend/src/variables.scss
@@ -7,11 +7,13 @@ $secondary-dark: #c40e21;
$on-secondary: #080808;
$contrast: #5396e7;
$input-border: #64619f;
-$border: #181553;
+$border: #0e0c3b;
+$border-light:#242079;
$background: #1d1a5a;
$background-light: #1a0f7a;
$background-dark: #0D0221;
+$skeleton: #150337;
$red: rgb(255, 79, 100);
$green: #41cf7c;
$rouge: #a6333f;
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index dcea99c..2e317f5 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -11,7 +11,12 @@ const config = {
}
}
},
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ css: true,
+ },
optimizeDeps: { exclude: ['svelte-navigator'] }
};