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-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-typescript": "^6.0.0",
|
||||
"@testing-library/svelte": "^3.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-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^6.0.0",
|
||||
"rollup-plugin-terser": "^7.0.0",
|
||||
"svelte": "^3.0.0",
|
||||
"svelte-check": "^1.0.0",
|
||||
"svelte-jester": "^1.1.5",
|
||||
"svelte-preprocess": "^4.0.0",
|
||||
"ts-jest": "^26.4.1",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.3"
|
||||
},
|
||||
|
|
|
@ -52,12 +52,14 @@ export function parseTaskId(id: string): ParsedTaskId | null {
|
|||
return { rocnik, z: !!z, serie, uloha }
|
||||
}
|
||||
|
||||
function getLocation(id: string, solution: boolean): TaskLocation | null {
|
||||
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
|
||||
if (!m) return null
|
||||
const [_, rocnik, z, serie, uloha] = m
|
||||
function getLocation(id: string, solution: boolean): TaskLocation {
|
||||
const parsedId = parseTaskId(id)
|
||||
if (!parsedId) {
|
||||
throw new Error("Can not parse " + id)
|
||||
}
|
||||
const { rocnik, z, serie, uloha } = parsedId
|
||||
const urlX = solution ? "reseni" : "zadani"
|
||||
if (z == 'Z') {
|
||||
if (z) {
|
||||
return {
|
||||
url: `/z/ulohy/${rocnik}/${urlX}${serie}.html`,
|
||||
startElement: `task-${id}`
|
||||
|
@ -79,7 +81,7 @@ function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentDat
|
|||
|
||||
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) {
|
||||
var [_, id, name, __, points] = ["", startElementId, "Neznámé jméno úlohy", "", ""]
|
||||
} else {
|
||||
|
@ -95,7 +97,7 @@ function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentDat
|
|||
while (!e.classList.contains("story") &&
|
||||
// !e.classList.contains("clearfloat") &&
|
||||
e.tagName.toLowerCase() != "h3" &&
|
||||
e.innerText.trim() != "Řešení"
|
||||
e.textContent!.trim() != "Řešení"
|
||||
)
|
||||
{
|
||||
elements.push(e)
|
||||
|
@ -112,7 +114,7 @@ function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentDat
|
|||
let r = ""
|
||||
for (const e of elements) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -132,10 +134,10 @@ function parseTaskStatuses(doc: HTMLDocument): TaskStatus[] {
|
|||
const rows = Array.from(doc.querySelectorAll("table.zs-tasklist tr")).slice(1) as HTMLTableRowElement[]
|
||||
return rows.map(r => {
|
||||
const submitted = !r.classList.contains("zs-unsubmitted")
|
||||
const id = r.cells[0].innerText.trim()
|
||||
const type = r.cells[1].innerText.trim()
|
||||
const name = r.cells[2].innerText.trim()
|
||||
const pointsStr = r.cells[4].innerText.trim()
|
||||
const id = r.cells[0].textContent!.trim()
|
||||
const type = r.cells[1].textContent!.trim()
|
||||
const name = r.cells[2].textContent!.trim()
|
||||
const pointsStr = r.cells[4].textContent!.trim()
|
||||
const pointsMatch = /((–|\.|\d)+) *\/ *(\d+)/.exec(pointsStr)
|
||||
if (!pointsMatch) throw new Error()
|
||||
const points = +pointsMatch[1]
|
||||
|
@ -166,16 +168,6 @@ async function loadTask({ url, startElement }: TaskLocation): Promise<TaskAssign
|
|||
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 {
|
||||
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> {
|
||||
const l = getLocation(id, false)
|
||||
if (!l) return virtualTask(id)
|
||||
return await loadTask(l)
|
||||
return await loadTask(getLocation(id, false))
|
||||
}
|
||||
|
||||
export async function grabSolution(id: string): Promise<TaskAssignmentData> {
|
||||
const l = getLocation(id, true)
|
||||
if (!l) return virtualTask(id)
|
||||
return await loadTask(l)
|
||||
return await loadTask(getLocation(id, true))
|
||||
}
|
||||
|
|
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",
|
||||
"compilerOptions": {
|
||||
// "target": "ES2019"
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
|
||||
"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