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