add jest tests for ksp task grabbing
This commit is contained in:
parent
8163ce94ab
commit
460d670e75
7 changed files with 3543 additions and 47 deletions
32
frontend/.vscode/launch.json
vendored
Normal file
32
frontend/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - All",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["--runInBand"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Jest - Current",
|
||||||
|
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||||
|
"args": ["${relativeFile}"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"windows": {
|
||||||
|
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
9
frontend/jest.config.js
Normal file
9
frontend/jest.config.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts?$': 'ts-jest',
|
||||||
|
'^.+\\.svelte$': 'svelte-jester',
|
||||||
|
},
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
testRegex: '.*\\.test?\\.ts$',
|
||||||
|
moduleFileExtensions: ['ts', 'js', 'svelte']
|
||||||
|
};
|
|
@ -12,14 +12,20 @@
|
||||||
"@rollup/plugin-commonjs": "^14.0.0",
|
"@rollup/plugin-commonjs": "^14.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||||
"@rollup/plugin-typescript": "^6.0.0",
|
"@rollup/plugin-typescript": "^6.0.0",
|
||||||
|
"@testing-library/svelte": "^3.0.0",
|
||||||
"@tsconfig/svelte": "^1.0.0",
|
"@tsconfig/svelte": "^1.0.0",
|
||||||
|
"@types/jest": "^26.0.14",
|
||||||
|
"jest": "^26.5.3",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
"rollup": "^2.3.4",
|
"rollup": "^2.3.4",
|
||||||
"rollup-plugin-livereload": "^2.0.0",
|
"rollup-plugin-livereload": "^2.0.0",
|
||||||
"rollup-plugin-svelte": "^6.0.0",
|
"rollup-plugin-svelte": "^6.0.0",
|
||||||
"rollup-plugin-terser": "^7.0.0",
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
"svelte": "^3.0.0",
|
"svelte": "^3.0.0",
|
||||||
"svelte-check": "^1.0.0",
|
"svelte-check": "^1.0.0",
|
||||||
|
"svelte-jester": "^1.1.5",
|
||||||
"svelte-preprocess": "^4.0.0",
|
"svelte-preprocess": "^4.0.0",
|
||||||
|
"ts-jest": "^26.4.1",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"typescript": "^3.9.3"
|
"typescript": "^3.9.3"
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,12 +52,14 @@ export function parseTaskId(id: string): ParsedTaskId | null {
|
||||||
return { rocnik, z: !!z, serie, uloha }
|
return { rocnik, z: !!z, serie, uloha }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLocation(id: string, solution: boolean): TaskLocation | null {
|
function getLocation(id: string, solution: boolean): TaskLocation {
|
||||||
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
|
const parsedId = parseTaskId(id)
|
||||||
if (!m) return null
|
if (!parsedId) {
|
||||||
const [_, rocnik, z, serie, uloha] = m
|
throw new Error("Can not parse " + id)
|
||||||
|
}
|
||||||
|
const { rocnik, z, serie, uloha } = parsedId
|
||||||
const urlX = solution ? "reseni" : "zadani"
|
const urlX = solution ? "reseni" : "zadani"
|
||||||
if (z == 'Z') {
|
if (z) {
|
||||||
return {
|
return {
|
||||||
url: `/z/ulohy/${rocnik}/${urlX}${serie}.html`,
|
url: `/z/ulohy/${rocnik}/${urlX}${serie}.html`,
|
||||||
startElement: `task-${id}`
|
startElement: `task-${id}`
|
||||||
|
@ -79,7 +81,7 @@ function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentDat
|
||||||
|
|
||||||
let e = titleElement
|
let e = titleElement
|
||||||
|
|
||||||
const titleMatch = /^(\d+-Z?\d+-\d+) (.*?)( \((\d+) bod.*\))?$/.exec(e.innerText.trim())
|
const titleMatch = /^(\d+-Z?\d+-\d+) (.*?)( \((\d+) bod.*\))?$/.exec(e.textContent!.trim())
|
||||||
if (!titleMatch) {
|
if (!titleMatch) {
|
||||||
var [_, id, name, __, points] = ["", startElementId, "Neznámé jméno úlohy", "", ""]
|
var [_, id, name, __, points] = ["", startElementId, "Neznámé jméno úlohy", "", ""]
|
||||||
} else {
|
} else {
|
||||||
|
@ -95,7 +97,7 @@ function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentDat
|
||||||
while (!e.classList.contains("story") &&
|
while (!e.classList.contains("story") &&
|
||||||
// !e.classList.contains("clearfloat") &&
|
// !e.classList.contains("clearfloat") &&
|
||||||
e.tagName.toLowerCase() != "h3" &&
|
e.tagName.toLowerCase() != "h3" &&
|
||||||
e.innerText.trim() != "Řešení"
|
e.textContent!.trim() != "Řešení"
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
elements.push(e)
|
elements.push(e)
|
||||||
|
@ -112,7 +114,7 @@ function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentDat
|
||||||
let r = ""
|
let r = ""
|
||||||
for (const e of elements) {
|
for (const e of elements) {
|
||||||
// hack: remove the paragraph with the matching text. Occurs in KSP-H, but is useless in this context.
|
// hack: remove the paragraph with the matching text. Occurs in KSP-H, but is useless in this context.
|
||||||
if (e.innerText.trim().replace(/\s+/g, " ") == "Toto je praktická open-data úloha. V odevzdávacím systému si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.") {
|
if (e.textContent!.trim().replace(/\s+/g, " ") == "Toto je praktická open-data úloha. V odevzdávacím systému si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,10 +134,10 @@ function parseTaskStatuses(doc: HTMLDocument): TaskStatus[] {
|
||||||
const rows = Array.from(doc.querySelectorAll("table.zs-tasklist tr")).slice(1) as HTMLTableRowElement[]
|
const rows = Array.from(doc.querySelectorAll("table.zs-tasklist tr")).slice(1) as HTMLTableRowElement[]
|
||||||
return rows.map(r => {
|
return rows.map(r => {
|
||||||
const submitted = !r.classList.contains("zs-unsubmitted")
|
const submitted = !r.classList.contains("zs-unsubmitted")
|
||||||
const id = r.cells[0].innerText.trim()
|
const id = r.cells[0].textContent!.trim()
|
||||||
const type = r.cells[1].innerText.trim()
|
const type = r.cells[1].textContent!.trim()
|
||||||
const name = r.cells[2].innerText.trim()
|
const name = r.cells[2].textContent!.trim()
|
||||||
const pointsStr = r.cells[4].innerText.trim()
|
const pointsStr = r.cells[4].textContent!.trim()
|
||||||
const pointsMatch = /((–|\.|\d)+) *\/ *(\d+)/.exec(pointsStr)
|
const pointsMatch = /((–|\.|\d)+) *\/ *(\d+)/.exec(pointsStr)
|
||||||
if (!pointsMatch) throw new Error()
|
if (!pointsMatch) throw new Error()
|
||||||
const points = +pointsMatch[1]
|
const points = +pointsMatch[1]
|
||||||
|
@ -166,16 +168,6 @@ async function loadTask({ url, startElement }: TaskLocation): Promise<TaskAssign
|
||||||
return parseTask(startElement, html)
|
return parseTask(startElement, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
function virtualTask(id: string): TaskAssignmentData {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
description: "úloha je virtuální a neexistuje",
|
|
||||||
name: id,
|
|
||||||
points: 0,
|
|
||||||
titleHtml: "<h3>Virtuální úloha</h3>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLoggedIn(): boolean {
|
export function isLoggedIn(): boolean {
|
||||||
return !!document.querySelector(".auth a[href='/profil/profil.cgi']")
|
return !!document.querySelector(".auth a[href='/profil/profil.cgi']")
|
||||||
}
|
}
|
||||||
|
@ -196,13 +188,9 @@ export async function grabTaskStates(kspIds: string[]): Promise<Map<string, Task
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function grabAssignment(id: string): Promise<TaskAssignmentData> {
|
export async function grabAssignment(id: string): Promise<TaskAssignmentData> {
|
||||||
const l = getLocation(id, false)
|
return await loadTask(getLocation(id, false))
|
||||||
if (!l) return virtualTask(id)
|
|
||||||
return await loadTask(l)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function grabSolution(id: string): Promise<TaskAssignmentData> {
|
export async function grabSolution(id: string): Promise<TaskAssignmentData> {
|
||||||
const l = getLocation(id, true)
|
return await loadTask(getLocation(id, true))
|
||||||
if (!l) return virtualTask(id)
|
|
||||||
return await loadTask(l)
|
|
||||||
}
|
}
|
||||||
|
|
76
frontend/src/tests/grabber.test.ts
Normal file
76
frontend/src/tests/grabber.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import * as g from '../ksp-task-grabber'
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import type { TaskDescriptor, TasksFile } from '../tasks';
|
||||||
|
|
||||||
|
const node_fetch: any = require('node-fetch')
|
||||||
|
|
||||||
|
global.fetch = function(url: string, init: any) {
|
||||||
|
return node_fetch(new URL(url, "https://ksp.mff.cuni.cz").href, init)
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const tasks_json = readFileSync("../tasks.json").toString()
|
||||||
|
const tasks: TasksFile = JSON.parse(tasks_json)
|
||||||
|
|
||||||
|
|
||||||
|
describe('tasks.json validation', () => {
|
||||||
|
test("unique ids", () => {
|
||||||
|
const allIds = tasks.tasks.map(t => t.id)
|
||||||
|
// no duplicate ids
|
||||||
|
expect(allIds).toStrictEqual(Array.from(new Set(allIds).keys()))
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const t of tasks.tasks) {
|
||||||
|
test(`'${t.id}' is valid`, () => {
|
||||||
|
expect(t.id).not.toBe("")
|
||||||
|
expect(typeof t.id).toBe("string")
|
||||||
|
expect(t.title).toBeTruthy()
|
||||||
|
expect(t.position).toBeDefined()
|
||||||
|
expect(["open-data", "text", "label"]).toContain(t.type)
|
||||||
|
if (t.type == "text") {
|
||||||
|
expect(t.htmlContent?.trim()).toBeTruthy()
|
||||||
|
} else if (t.type == "open-data") {
|
||||||
|
expect(t.taskReference).toBeTruthy()
|
||||||
|
expect(g.parseTaskId(t.taskReference)).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('tasks assignment', () => {
|
||||||
|
for (const t of tasks.tasks) {
|
||||||
|
if (t.type != "open-data") continue;
|
||||||
|
|
||||||
|
test(`${t.id}`, async () => {
|
||||||
|
const assignment = await g.grabAssignment((t as any).taskReference)
|
||||||
|
expect((t as any).taskReference).toEqual(assignment.id)
|
||||||
|
expect(assignment.points).toBeGreaterThanOrEqual(1)
|
||||||
|
expect(assignment.description.trim()).toBeTruthy()
|
||||||
|
expect(assignment.name.trim()).toBeTruthy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('tasks solutions', () => {
|
||||||
|
const refs = tasks.tasks.filter(x => x.type == "open-data")
|
||||||
|
.map(x => g.parseTaskId((x as any).taskReference)!)
|
||||||
|
const lastSeriesZ = Math.max(... refs.filter(t => t.z).map(x => 10 * +x.rocnik + +x.serie))
|
||||||
|
const lastSeriesH = Math.max(... refs.filter(t => !t.z).map(x => 10 * +x.rocnik + +x.serie))
|
||||||
|
for (const t of tasks.tasks) {
|
||||||
|
if (t.type != "open-data") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = g.parseTaskId(t.taskReference)!
|
||||||
|
if (10 * +parsed.rocnik + +parsed.serie >= (parsed.z ? lastSeriesZ : lastSeriesH)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
test(`${t.id}`, async () => {
|
||||||
|
const sol = await g.grabSolution(t.taskReference)
|
||||||
|
expect(t.taskReference).toEqual(sol.id)
|
||||||
|
expect(sol.description.trim()).toBeTruthy()
|
||||||
|
expect(sol.name.trim()).toBeTruthy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
|
@ -2,7 +2,8 @@
|
||||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// "target": "ES2019"
|
// "target": "ES2019"
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"types": ["jest", "node"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
|
|
3420
frontend/yarn.lock
3420
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
Reference in a new issue