test : changed test format + added playwright support

This commit is contained in:
2026-05-03 21:52:02 +02:00
committed by djalim
parent 08894730a3
commit a6042087dc
52 changed files with 3576 additions and 5212 deletions
@@ -2,98 +2,19 @@
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
import { useEffect, useRef } from "preact/hooks";
import { useSignal } from "@preact/signals";
import {
parseMaquette,
type ParsedModule,
type ParsedUE,
type ParsedYear,
} from "$root/logic/maquette.ts";
import ImportResultPopup, {
type ImportDetail,
type ImportResult,
} from "$root/defaults/ImportResultPopup.tsx";
type ParsedUE = {
code: string | null;
name: string;
ects: number | null;
modules: ParsedModule[];
};
type ParsedModule = {
code: string;
name: string;
coeff: number;
};
type ParsedYear = {
label: string;
ues: ParsedUE[];
};
type Promo = { id: string; annee: string | null };
function parseMaquette(workbook: XLSX.WorkBook): ParsedYear[] {
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(sheet, {
header: 1,
});
const years: ParsedYear[] = [];
let currentYear: ParsedYear | null = null;
let currentUE: ParsedUE | null = null;
let moduleIndex = 0;
for (const row of rows) {
if (!row || row.length === 0) continue;
const col0 = row[0] != null ? String(row[0]).trim() : "";
// Detect year row: INFO3A, INFO4A, INFO5A, INFOAPP3A, INFOAPP4A, etc.
if (/^(INFO|INFOAPP)\s*\d+A$/i.test(col0)) {
currentYear = { label: col0, ues: [] };
years.push(currentYear);
currentUE = null;
continue;
}
// Detect UE row: col[0] === "UE" or starts with "UE" (e.g., "UE51")
if (col0 === "UE" || (col0.startsWith("UE") && /^UE\d/.test(col0))) {
const ueCode = row[1] != null ? String(row[1]).trim() : null;
const ueName = row[2] != null ? String(row[2]).trim() : "UE sans nom";
const ects = typeof row[4] === "number" ? row[4] : null;
currentUE = { code: ueCode, name: ueName, ects, modules: [] };
if (currentYear) {
currentYear.ues.push(currentUE);
} else {
// No year detected yet — create a default one
currentYear = { label: "Maquette", ues: [currentUE] };
years.push(currentYear);
}
moduleIndex = 0;
continue;
}
// Detect semester header rows — just skip, don't reset UE
if (/^SEM\s*\d/i.test(col0)) {
currentUE = null;
continue;
}
// Detect module row: inside a UE, col[3] has a name, col[5] is a number (coeff)
if (currentUE && row[3] != null && typeof row[5] === "number") {
const modName = String(row[3]).trim();
if (!modName) continue;
let modCode = row[1] != null ? String(row[1]).trim() : "";
if (!modCode) {
const uePrefix = (currentUE.code || "MOD").replace(/[^A-Z0-9]/gi, "");
modCode = `${uePrefix}_${String(moduleIndex).padStart(2, "0")}`;
}
currentUE.modules.push({ code: modCode, name: modName, coeff: row[5] });
moduleIndex++;
}
}
return years;
}
export default function ImportMaquette() {
const file = useSignal<File | null>(null);
const dragging = useSignal(false);
+27 -23
View File
@@ -2,6 +2,11 @@
import * as XLSX from "https://cdn.sheetjs.com/xlsx-0.20.3/package/xlsx.mjs";
import { useEffect, useRef } from "preact/hooks";
import { useSignal } from "@preact/signals";
import {
calculateWeightedAverage,
getEffectiveNote,
roundGrade,
} from "$root/logic/grades.ts";
import ImportResultPopup, {
type ImportDetail,
type ImportResult,
@@ -393,19 +398,19 @@ export default function ImportNotes() {
const ueMods = orderedCols.filter(
(c) => c.type === "module" && c.ueId === col.ueId,
);
let total = 0, coeffSum = 0;
for (const um of ueMods) {
const notesRecord: Record<string, { note: number; noteSession2: number | null }> = {};
ueMods.forEach(um => {
const n = sNotes.get(um.id);
if (n && um.coeff) {
total += n.note * um.coeff;
coeffSum += um.coeff;
}
}
row.push(
coeffSum > 0
? Math.round((total / coeffSum) * 100) / 100
: null,
if (n) notesRecord[um.id] = n;
});
const avg = calculateWeightedAverage(
ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })),
notesRecord
);
row.push(avg !== null ? roundGrade(avg) : null);
}
}
s1Rows.push(row);
@@ -425,20 +430,19 @@ export default function ImportNotes() {
const ueMods = orderedCols.filter(
(c) => c.type === "module" && c.ueId === col.ueId,
);
let total = 0, coeffSum = 0;
for (const um of ueMods) {
const notesRecord: Record<string, { note: number; noteSession2: number | null }> = {};
ueMods.forEach(um => {
const n = sNotes.get(um.id);
if (n && um.coeff) {
const noteVal = n.noteSession2 ?? n.note;
total += noteVal * um.coeff;
coeffSum += um.coeff;
}
}
row.push(
coeffSum > 0
? Math.round((total / coeffSum) * 100) / 100
: null,
if (n) notesRecord[um.id] = n;
});
const avg = calculateWeightedAverage(
ueMods.map(m => ({ idModule: m.id, coeff: m.coeff ?? 0 })),
notesRecord
);
row.push(avg !== null ? roundGrade(avg) : null);
}
}
s2Rows.push(row);
+12 -31
View File
@@ -1,6 +1,12 @@
import { useEffect, useState } from "preact/hooks";
import {
applyAjustement,
calculateWeightedAverage,
getEffectiveNote,
} from "$root/logic/grades.ts";
type Student = {
// ...
numEtud: number;
nom: string;
prenom: string;
@@ -37,11 +43,6 @@ function noteClass(n: number): string {
return n >= 10 ? "note-chip note-chip--ok" : "note-chip note-chip--fail";
}
/** Returns the effective note (session 2 if exists, otherwise session 1). */
function effectiveNote(n: Note): number {
return n.noteSession2 ?? n.note;
}
export default function NoteRecap({ numEtud }: Props) {
const [student, setStudent] = useState<Student | null>(null);
const [ueList, setUeList] = useState<UE[]>([]);
@@ -108,19 +109,6 @@ export default function NoteRecap({ numEtud }: Props) {
load();
}, [numEtud]);
function calcAvg(ueMods: UEModule[]): number | null {
let total = 0,
coeff = 0;
for (const um of ueMods) {
const n = noteMap.get(um.idModule);
if (n === undefined) return null;
const val = effectiveNote(n);
total += val * um.coeff;
coeff += um.coeff;
}
return coeff > 0 ? total / coeff : null;
}
async function saveNote(
idModule: string,
field: "note" | "noteSession2",
@@ -280,18 +268,11 @@ export default function NoteRecap({ numEtud }: Props) {
</p>
)
: ueList.map((ue) => {
const ueMods = ueModules.filter((um) => um.idUE === ue.id);
const avg = calcAvg(ueMods);
const ajust = ajustements.find((a) => a.idUE === ue.id);
// Final displayed average: if ajust.valeur exists it replaces avg, then subtract malus
let finalAvg = avg;
if (ajust) {
finalAvg = ajust.valeur;
if (ajust.malus > 0) {
finalAvg = (finalAvg ?? 0) - ajust.malus;
}
}
const ueMods = ueList.length > 0 ? ueModules.filter((um) => um.idUE === ue.id) : [];
const notesRecord = Object.fromEntries(noteMap);
const avg = calculateWeightedAverage(ueMods, notesRecord);
const ajust = ajustements.find((a) => a.idUE === ue.id) ?? null;
const finalAvg = applyAjustement(avg, ajust);
return (
<div key={ue.id} class="edit-section">
@@ -341,7 +322,7 @@ export default function NoteRecap({ numEtud }: Props) {
const noteVal = noteObj?.note;
const noteS2 = noteObj?.noteSession2;
const effective = noteObj
? effectiveNote(noteObj)
? getEffectiveNote(noteObj)
: undefined;
const nomMod = moduleMap.get(um.idModule) ?? um.idModule;
+9 -28
View File
@@ -1,4 +1,9 @@
import { useEffect, useState } from "preact/hooks";
import {
applyAjustement,
calculateWeightedAverage,
getEffectiveNote,
} from "$root/logic/grades.ts";
type Note = {
numEtud: number;
@@ -6,6 +11,7 @@ type Note = {
note: number;
noteSession2: number | null;
};
// ... rest of types unchanged ...
type UE = { id: number; nom: string };
type UEModule = {
idModule: string;
@@ -36,11 +42,6 @@ function avgClass(avg: number | null): string {
return avg >= 10 ? "avg-good" : "avg-warn";
}
/** Returns the effective note (session 2 if exists, otherwise session 1). */
function effectiveNote(n: Note): number {
return n.noteSession2 ?? n.note;
}
export default function NotesView({ numEtud, prenom }: Props) {
const [notes, setNotes] = useState<Note[]>([]);
const [ues, setUes] = useState<UE[]>([]);
@@ -175,29 +176,9 @@ export default function NotesView({ numEtud, prenom }: Props) {
if (!ue) return null;
const ueModsForUE = filteredUeModules.filter((um) => um.idUE === ueId);
let weightedSum = 0;
let coveredCoeff = 0;
ueModsForUE.forEach((um) => {
const noteObj = noteMap[um.idModule];
if (noteObj) {
const val = effectiveNote(noteObj);
weightedSum += val * um.coeff;
coveredCoeff += um.coeff;
}
});
const avg = coveredCoeff > 0 ? weightedSum / coveredCoeff : null;
const avg = calculateWeightedAverage(ueModsForUE, noteMap);
const ajust = ajMap[ueId] ?? null;
// If ajust.valeur exists, it replaces the calculated average
// Then malus is subtracted
let finalAvg: number | null = avg;
if (ajust) {
finalAvg = ajust.valeur;
if (ajust.malus > 0) {
finalAvg = (finalAvg ?? 0) - ajust.malus;
}
}
const finalAvg = applyAjustement(avg, ajust);
return (
<div key={ueId} class="ue-card">
@@ -218,7 +199,7 @@ export default function NotesView({ numEtud, prenom }: Props) {
{ueModsForUE.map((um) => {
const mod = moduleMap[um.idModule];
const noteObj = noteMap[um.idModule] ?? null;
const effective = noteObj ? effectiveNote(noteObj) : null;
const effective = noteObj ? getEffectiveNote(noteObj) : null;
const hasS2 = noteObj?.noteSession2 != null;
return (