Browse Source

add jest tests for ksp task grabbing

mj-deploy
Standa Lukeš 4 years ago
parent
commit
460d670e75
  1. 32
      frontend/.vscode/launch.json
  2. 9
      frontend/jest.config.js
  3. 6
      frontend/package.json
  4. 44
      frontend/src/ksp-task-grabber.ts
  5. 76
      frontend/src/tests/grabber.test.ts
  6. 3
      frontend/tsconfig.json
  7. 3548
      frontend/yarn.lock

32
frontend/.vscode/launch.json

@ -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

@ -0,0 +1,9 @@
module.exports = {
transform: {
'^.+\\.ts?$': 'ts-jest',
'^.+\\.svelte$': 'svelte-jester',
},
testEnvironment: 'jsdom',
testRegex: '.*\\.test?\\.ts$',
moduleFileExtensions: ['ts', 'js', 'svelte']
};

6
frontend/package.json

@ -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"
}, },

44
frontend/src/ksp-task-grabber.ts

@ -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

@ -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()
})
}
})

3
frontend/tsconfig.json

@ -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/**/*"],

3548
frontend/yarn.lock

File diff suppressed because it is too large