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