chore: run prettier through eslint (#2070)

This commit is contained in:
Daniel Kantor 2022-03-15 08:09:37 +01:00 committed by GitHub
parent eba232b43a
commit 4ea85181da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 1654 additions and 1614 deletions

View File

@ -1,50 +1,55 @@
module.exports = {
parser: "@typescript-eslint/parser",
ignorePatterns: ["node_modules/"],
env: {
browser: true,
es6: true,
node: true,
"jest/globals": true,
"cypress/globals": true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
process: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
extraFileExtensions: [".svelte"],
},
plugins: ["svelte3", "jest", "cypress", "@typescript-eslint"],
overrides: [
{
files: ["**/*.svelte"],
processor: "svelte3/svelte3",
rules: {
"import/first": "off",
"import/no-duplicates": "off",
"import/no-mutable-exports": "off",
"import/no-unresolved": "off",
},
},
],
rules: {
"@typescript-eslint/no-unused-vars": "error",
indent: ["error", 4],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "never"],
},
settings: {
"svelte3/ignore-styles": () => true,
"svelte3/typescript": require("typescript"),
parser: "@typescript-eslint/parser",
ignorePatterns: [
"node_modules/",
"dist",
"__sapper__",
"build",
"jest-coverage",
"coverage",
],
env: {
browser: true,
es6: true,
node: true,
"jest/globals": true,
"cypress/globals": true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
globals: {
Atomics: "readonly",
SharedArrayBuffer: "readonly",
process: true,
},
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
extraFileExtensions: [".svelte"],
},
plugins: ["svelte3", "jest", "cypress", "@typescript-eslint", "prettier"],
overrides: [
{
files: ["**/*.svelte"],
processor: "svelte3/svelte3",
rules: {
"import/first": "off",
"import/no-duplicates": "off",
"import/no-mutable-exports": "off",
"import/no-unresolved": "off",
},
},
],
rules: {
"prettier/prettier": ["error", { semi: false }],
"@typescript-eslint/no-unused-vars": "error",
"linebreak-style": ["error", "unix"],
},
settings: {
"svelte3/ignore-styles": () => true,
"svelte3/typescript": require("typescript"),
},
}

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
*.svelte

View File

@ -65,7 +65,7 @@ blocks:
- sem-version node 14
- checkout
- cache restore node-$(checksum yarn.lock)
- yarn eslint apps/*/src --no-error-on-unmatched-pattern
- yarn lint
- name: Eslint Cypress
dependencies:
- Set up dependencies
@ -76,7 +76,7 @@ blocks:
- sem-version node 14
- checkout
- cache restore node-$(checksum yarn.lock)
- yarn eslint apps/*/cypress
- yarn lint
- name: TypeScript check
dependencies:
- Eslint

View File

@ -1,8 +0,0 @@
{
"tabWidth": 2,
"svelteSortOrder" : "scripts-markup-styles",
"svelteStrictMode": true,
"allowShorthand": false,
"plugins": ["prettier-plugin-svelte"],
"semi": false
}

View File

@ -1,46 +1,46 @@
import evaluateAnswer from "."
describe("evaluateAnswer", () => {
it("returns correct value when answer is correct", () => {
expect(
evaluateAnswer({ answer: "foo", validAnswers: ["baz", "foo", "bar"] })
).toEqual({ correct: true, suggestion: "" })
})
it("returns correct value when answer is correct", () => {
expect(
evaluateAnswer({ answer: "foo", validAnswers: ["baz", "foo", "bar"] })
).toEqual({ correct: true, suggestion: "" })
})
it("returns correct value when answer is incorrect", () => {
expect(
evaluateAnswer({ answer: "foo", validAnswers: ["baz", "bar"] })
).toEqual({ correct: false, suggestion: "" })
})
it("returns correct value when answer is incorrect", () => {
expect(
evaluateAnswer({ answer: "foo", validAnswers: ["baz", "bar"] })
).toEqual({ correct: false, suggestion: "" })
})
it("returns correct suggestion", () => {
expect(
evaluateAnswer({ answer: "ba", validAnswers: ["foo", "bar"] })
).toEqual({
correct: true,
suggestion: "Correct spelling: bar"
})
it("returns correct suggestion", () => {
expect(
evaluateAnswer({ answer: "ba", validAnswers: ["foo", "bar"] })
).toEqual({
correct: true,
suggestion: "Correct spelling: bar",
})
})
it("returns no suggestion when there's a correct form", () => {
expect(
evaluateAnswer({ answer: "ba", validAnswers: ["ba", "foo", "bar",] })
).toEqual({
correct: true,
suggestion: ""
})
it("returns no suggestion when there's a correct form", () => {
expect(
evaluateAnswer({ answer: "ba", validAnswers: ["ba", "foo", "bar"] })
).toEqual({
correct: true,
suggestion: "",
})
})
it("returns correct suggestion - punctuation", () => {
expect(
evaluateAnswer({
answer: "foo bar lorem ipsum dolor sit amet baz",
validAnswers: ["foo", "¡foo bar lorem ipsum dolor sit amet baz!"]
})
).toEqual({
correct: true,
suggestion:
"Watch out for punctuation! Correct spelling: ¡foo bar lorem ipsum dolor sit amet baz!"
})
it("returns correct suggestion - punctuation", () => {
expect(
evaluateAnswer({
answer: "foo bar lorem ipsum dolor sit amet baz",
validAnswers: ["foo", "¡foo bar lorem ipsum dolor sit amet baz!"],
})
).toEqual({
correct: true,
suggestion:
"Watch out for punctuation! Correct spelling: ¡foo bar lorem ipsum dolor sit amet baz!",
})
})
})

View File

@ -2,89 +2,97 @@ import levenshtein from "js-levenshtein"
const id = (x: string): string => x
const ignorePunctuation = (form: string): string => form.replace(/[!¡?¿,.]/g, "")
const ignorePunctuation = (form: string): string =>
form.replace(/[!¡?¿,.]/g, "")
const ignoreCasing = (form: string): string => form.toLowerCase()
const ignoreWhitespace = (form: string): string =>
form.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " ")
form.replace(/^\s+|\s+$/g, "").replace(/\s+/g, " ")
const normalize = (form: string): string => ignoreWhitespace(ignoreCasing(form))
const areSentencesSimilar = (sentence1: string, sentence2: string): boolean =>
levenshtein(normalize(sentence1), normalize(sentence2)) <= 1
levenshtein(normalize(sentence1), normalize(sentence2)) <= 1
const areSentencesIdentical = (sentence1: string, sentence2: string): boolean =>
normalize(sentence1) === normalize(sentence2)
normalize(sentence1) === normalize(sentence2)
const getSuggestion = ({
alwaysSuggest,
answer,
mappedForm,
suggester,
form
alwaysSuggest,
answer,
mappedForm,
suggester,
form,
}: {
alwaysSuggest?: boolean,
answer: string,
mappedForm: string,
suggester: (form: string) => string,
form: string,
alwaysSuggest?: boolean
answer: string
mappedForm: string
suggester: (form: string) => string
form: string
}): string =>
!alwaysSuggest && areSentencesIdentical(answer, mappedForm)
? ""
: suggester(form)
!alwaysSuggest && areSentencesIdentical(answer, mappedForm)
? ""
: suggester(form)
const evaluateAnswerRaw = ({
validAnswers,
answer,
suggester,
alwaysSuggest,
mapper,
}: {
validAnswers: string[]
answer: string
suggester: (form: string) => string
alwaysSuggest?: boolean
mapper: (form: string) => string
}): { correct: boolean; suggestion: string } => {
let correct = false,
suggestion = ""
validAnswers.forEach((form) => {
const mappedForm = mapper(form)
if (areSentencesSimilar(answer, mappedForm)) {
if (correct && !suggestion) {
return
}
correct = true
suggestion = getSuggestion({
alwaysSuggest,
answer,
mappedForm,
suggester,
form,
})
}
})
return { correct, suggestion }
}
export default function evaluateAnswer({
validAnswers,
answer,
}: {
validAnswers: string[]
answer: string
}): { correct: boolean; suggestion: string } {
let result = evaluateAnswerRaw({
mapper: id,
validAnswers,
answer,
suggester,
alwaysSuggest,
mapper
}: {
validAnswers: string[],
answer: string,
suggester: (form: string) => string,
alwaysSuggest?: boolean,
mapper: (form: string) => string,
}): { correct: boolean; suggestion: string; } => {
let correct = false,
suggestion = ""
suggester: (form) => `Correct spelling: ${form}`,
})
validAnswers.forEach(form => {
const mappedForm = mapper(form)
if (areSentencesSimilar(answer, mappedForm)) {
if (correct && !suggestion) {
return
}
correct = true
suggestion = getSuggestion({
alwaysSuggest,
answer,
mappedForm,
suggester,
form
})
}
if (!result.correct) {
result = evaluateAnswerRaw({
alwaysSuggest: true,
validAnswers,
mapper: ignorePunctuation,
answer,
suggester: (form) =>
`Watch out for punctuation! Correct spelling: ${form}`,
})
}
return { correct, suggestion }
}
export default function evaluateAnswer({ validAnswers, answer }: { validAnswers: string[], answer: string }): { correct: boolean; suggestion: string; } {
let result = evaluateAnswerRaw({
mapper: id,
validAnswers,
answer,
suggester: form => `Correct spelling: ${form}`
})
if (!result.correct) {
result = evaluateAnswerRaw({
alwaysSuggest: true,
validAnswers,
mapper: ignorePunctuation,
answer,
suggester: form => `Watch out for punctuation! Correct spelling: ${form}`
})
}
return result
return result
}

View File

@ -1,4 +1,3 @@
<div class="box">
<slot />
</div>

View File

@ -28,7 +28,7 @@
</div>
{#if asHref != null}
<a class="hidden-link" href={asHref} />
<a class="hidden-link" href={asHref}>&nbsp;</a>
{/if}
<style type="text/scss">

View File

@ -1,14 +1,16 @@
<script lang="typescript">
import Icon from "lluis/Icon.svelte"
import Icon from "lluis/Icon.svelte";
export let name: string
export let id: string
export let icon: string
export let type = "text"
export let value: string | boolean | number
export let formStatus = {}
let error = null
$: {error = formStatus[id]}
export let name: string;
export let id: string;
export let icon: string;
export let type = "text";
export let value: string | boolean | number;
export let formStatus = {};
let error = null;
$: {
error = formStatus[id];
}
</script>
<div class="field">
@ -19,20 +21,22 @@
class="input"
type="text"
name={id}
id={id}
class:is-danger={error != null}
bind:value="{value}" />
{id}
class:is-danger={error != null}
bind:value
/>
{/if}
{#if type === "password"}
<input
class="input"
type="password"
name={id}
id={id}
class:is-danger={error != null}
bind:value="{value}" />
{id}
class:is-danger={error != null}
bind:value
/>
{/if}
<Icon size="small" icon={icon} left />
<Icon size="small" {icon} left />
</div>
{#if error != null}
<p class="help is-danger">{error}</p>

View File

@ -1,5 +1,5 @@
<nav
role="navigation"
data-test-id="navbar"
aria-label="main navigation"
>

View File

@ -3,8 +3,6 @@
"private": true,
"version": "0.0.0",
"scripts": {
"eslintfix": "exit 0",
"prettierfix": "exit 0",
"build": "exit 0",
"types": "exit 0"
},

View File

@ -1,5 +1,5 @@
module.exports = {
process(src, filename, config, options) {
return
},
process() {
return
},
}

View File

@ -1,35 +1,29 @@
import { Given, Then } from "cypress-cucumber-preprocessor/steps"
Then("I see {int} cards", n => {
cy.get(".options")
.find(".card:visible")
.should("have.length", n)
Then("I see {int} cards", (n) => {
cy.get(".options").find(".card:visible").should("have.length", n)
})
Then("I see {int} inactive cards", n => {
cy.get(".options")
.find(".card[data-test=neutral]:visible, .card[data-test=inactive]:visible")
.should("have.length", n)
Then("I see {int} inactive cards", (n) => {
cy.get(".options")
.find(".card[data-test=neutral]:visible, .card[data-test=inactive]:visible")
.should("have.length", n)
})
Then("I see an active card", () => {
cy.get(".options")
.find(".card[data-test=active]:visible")
.should("have.length", 1)
cy.get(".options")
.find(".card[data-test=active]:visible")
.should("have.length", 1)
})
Given("I click a card", () => {
cy.get(".card:visible")
.first()
.click()
cy.get(".card:visible").first().click()
})
Given("I click the correct card", () => {
cy.get(".card[data-test-correct=true]").click()
cy.get(".card[data-test-correct=true]").click()
})
Given("I click an incorrect card", () => {
cy.get(".real .card[data-test-correct=false]")
.first()
.click()
cy.get(".real .card[data-test-correct=false]").first().click()
})

View File

@ -1,15 +1,15 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("words with definitions have tooltips", () => {
cy.get(".has-tooltip-bottom[data-tooltip=how]").should("be.visible")
cy.get(".has-tooltip-bottom[data-tooltip=how]").should("be.visible")
})
Then("I see the correct chips", () => {
cy.get(".chip").contains("Como").should("be.visible")
cy.get(".chip").contains("estás").should("be.visible")
cy.get(".chip").contains("hoy").should("be.visible")
cy.get(".chip").contains("Como").should("be.visible")
cy.get(".chip").contains("estás").should("be.visible")
cy.get(".chip").contains("hoy").should("be.visible")
})
Then("I have unused chips", () => {
cy.get("#chips .chip").should("be.visible")
cy.get("#chips .chip").should("be.visible")
})

View File

@ -1,13 +1,13 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I am logged out", () => {
cy.window().then((window) => {
cy.wrap(window._Logout())
})
cy.window().then((window) => {
cy.wrap(window._Logout())
})
})
Then("user already exists", () => {
cy.window().then((window) => {
window._test_user_already_exists = true
})
cy.window().then((window) => {
window._test_user_already_exists = true
})
})

View File

@ -1,8 +1,15 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("the \"Feedback\" button opens the course repository page in a new tab", () => {
Then(
'the "Feedback" button opens the course repository page in a new tab',
() => {
const feedbackButton = cy.contains("Feedback")
feedbackButton.should("have.attr", "target", "_blank")
feedbackButton.should("have.attr", "href", "https://github.com/LibreLingo/LibreLingo/")
feedbackButton.should(
"have.attr",
"href",
"https://github.com/LibreLingo/LibreLingo/"
)
feedbackButton.should("be.visible")
})
}
)

View File

@ -2,48 +2,63 @@ import { Then } from "cypress-cucumber-preprocessor/steps"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Then("{} looks correct", (snapshotsName) => {
cy.get(".fontawesome-i2svg-pending").should("not.exist")
cy.document().its("fonts.status").should("equal", "loaded")
cy.get(".fontawesome-i2svg-pending").should("not.exist")
cy.document().its("fonts.status").should("equal", "loaded")
// Normalize problematic pages to avoid flaky tests
switch(snapshotsName) {
// Normalize problematic pages to avoid flaky tests
switch (snapshotsName) {
case "cards challenge":
cy.get("[data-test=\"card-img-1\"]").invoke("attr", "src", "images/pasta1.jpg")
cy.get("[data-test=\"card-img-2\"]").invoke("attr", "src", "images/dog1.jpg")
cy.get("[data-test=\"card-img-3\"]").invoke("attr", "src", "images/cat3.jpg")
cy.get(".fake img").invoke("attr", "src", "images/cat3.jpg")
cy.window().then((win) => {
const fakeCard = win.document.querySelector(".fake")
const parent = fakeCard.parentElement
fakeCard.remove()
parent.append(fakeCard)
})
Cypress.$("[data-test=\"card-text-1\"]").text("pasta")
Cypress.$("[data-test=\"card-text-2\"]").text("dog")
Cypress.$("[data-test=\"card-text-3\"]").text("cat")
Cypress.$(".fake .card-text").text("cat")
Cypress.$("[data-test=\"meaning-in-source-language\"]").text("pasta")
break
cy.get('[data-test="card-img-1"]').invoke(
"attr",
"src",
"images/pasta1.jpg"
)
cy.get('[data-test="card-img-2"]').invoke(
"attr",
"src",
"images/dog1.jpg"
)
cy.get('[data-test="card-img-3"]').invoke(
"attr",
"src",
"images/cat3.jpg"
)
cy.get(".fake img").invoke("attr", "src", "images/cat3.jpg")
cy.window().then((win) => {
const fakeCard = win.document.querySelector(".fake")
const parent = fakeCard.parentElement
fakeCard.remove()
parent.append(fakeCard)
})
Cypress.$('[data-test="card-text-1"]').text("pasta")
Cypress.$('[data-test="card-text-2"]').text("dog")
Cypress.$('[data-test="card-text-3"]').text("cat")
Cypress.$(".fake .card-text").text("cat")
Cypress.$('[data-test="meaning-in-source-language"]').text("pasta")
break
case "chips challenge":
cy.get("#chips .chip .tag").each(($el, index) => {
$el.text(("tú hoy estás como usted eres".split(" "))[index])
})
Cypress.$("[data-test=\"meaning-in-source-language\"]").text("pasta")
break
cy.get("#chips .chip .tag").each(($el, index) => {
$el.text("tú hoy estás como usted eres".split(" ")[index])
})
Cypress.$('[data-test="meaning-in-source-language"]').text("pasta")
break
case "option selection challenge":
case "option selection challenge with first option selected":
cy.get(".option-content div").each(($el, index) => {
$el.text(("cerdo perro león".split(" "))[index])
})
Cypress.$("[data-test=\"meaning-in-source-language\"]").text("perro")
break
cy.get(".option-content div").each(($el, index) => {
$el.text("cerdo perro león".split(" ")[index])
})
Cypress.$('[data-test="meaning-in-source-language"]').text("perro")
break
case "short text input challenge":
cy.get("[data-test=\"short text input illustrations\"]")
.invoke("attr", "src", "images/dog1.jpg")
break
cy.get('[data-test="short text input illustrations"]').invoke(
"attr",
"src",
"images/dog1.jpg"
)
break
case "the terms of service page":
case "the license page":
@ -60,14 +75,14 @@ Then("{} looks correct", (snapshotsName) => {
case "course page with a stale skill":
case "course page":
case "listening challenge":
// These tests are deterministic and thus don't need normalization.
break
// These tests are deterministic and thus don't need normalization.
break
default:
throw new Error(`unhandled visual test "${snapshotsName}"`)
}
throw new Error(`unhandled visual test "${snapshotsName}"`)
}
cy.percySnapshot(snapshotsName)
cy.percySnapshot(snapshotsName)
cy.generateTranslationScreenshots()
cy.generateTranslationScreenshots()
})

View File

@ -1,5 +1,5 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given("I am on {string}", (url) => {
cy.url().should("include", url)
cy.url().should("include", url)
})

View File

@ -1,9 +1,9 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I click {string}", (text) => {
cy.contains(text).click()
cy.contains(text).click()
})
Then("I click the {string} button", (text) => {
cy.get(".lluis-button").contains(text).click()
cy.get(".lluis-button").contains(text).click()
})

View File

@ -1,5 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I enable the feature {string}", (featureName) => {
cy.setCookie(`${featureName}Enabled`, "true")
cy.setCookie(`${featureName}Enabled`, "true")
})

View File

@ -1,5 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I hit the enter key", () => {
cy.get("body").trigger("keydown", { keyCode: 13, which: 13 })
cy.get("body").trigger("keydown", { keyCode: 13, which: 13 })
})

View File

@ -1,9 +1,9 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I introduce {string} as {string}", (value, fieldName) => {
cy.get(`[name=${fieldName}]`).type(value)
cy.get(`[name=${fieldName}]`).type(value)
})
Then("I check the {string} checkbox", (fieldName) => {
cy.get(`[name=${fieldName}]`).check()
cy.get(`[name=${fieldName}]`).check()
})

View File

@ -1,5 +1,5 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given("I open {string}", url => {
cy.visit(url)
Given("I open {string}", (url) => {
cy.visit(url)
})

View File

@ -1,9 +1,9 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given("I read {string}", (text) => {
cy.contains(text).should("be.visible")
cy.contains(text).should("be.visible")
})
Given("I don't read {string}", (text) => {
cy.contains(text).should("not.exist")
cy.contains(text).should("not.exist")
})

View File

@ -1,22 +1,22 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I see an input field", () => {
cy.get("input[type=text]").should("be.visible")
cy.get("input[type=text]").should("be.visible")
})
Then("the input field is focused", () => {
cy.get("[data-test=answer]").should("be.focused")
cy.get("[data-test=answer]").should("be.focused")
})
Then("the input field has a {string} placeholder", (text) => {
cy.get("input[type=text]").first().should("have.attr", "placeholder", text)
cy.get("input[type=text]").first().should("have.attr", "placeholder", text)
})
Then("the {string} field has the label {string}", (fieldName, labelText) => {
cy.get(`label[for=${fieldName}]`).should("contain", labelText)
cy.get(`#${fieldName}`).should("be.visible")
cy.get(`label[for=${fieldName}]`).should("contain", labelText)
cy.get(`#${fieldName}`).should("be.visible")
})
Then("I see a {string} field", (fieldName) => {
cy.get(`[name=${fieldName}]`).should("be.visible")
cy.get(`[name=${fieldName}]`).should("be.visible")
})

View File

@ -1,9 +1,8 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given(/I see an? "(.*)" button/, (text) => {
cy.get(".lluis-button").contains(text).should("be.visible")
cy.get(".lluis-button").contains(text).should("be.visible")
})
Given(/I don't see an? "(.*)" button/, (text) => {
cy.get(".lluis-button").contains(text).should("not.exist")
cy.get(".lluis-button").contains(text).should("not.exist")
})

View File

@ -1,5 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I see a card with an image", () => {
cy.get(".card img").should("be.visible")
cy.get(".card img").should("be.visible")
})

View File

@ -1,6 +1,6 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I see a {} icon", icon => {
const iconClass = `fa-${icon.replace(" ", "-")}`
cy.get(`.${iconClass}`).should("be.visible")
Then("I see a {} icon", (icon) => {
const iconClass = `fa-${icon.replace(" ", "-")}`
cy.get(`.${iconClass}`).should("be.visible")
})

View File

@ -1,6 +1,5 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given(/I see an? "(.*)" link in the navbar/, (text) => {
cy.get("nav[role=navigation] a").contains(text).should("be.visible")
cy.get("[data-test-id=navbar] a").contains(text).should("be.visible")
})

View File

@ -1,37 +1,40 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I see the challenge panel with no Skip button", () => {
cy.get(".panel").should("be.visible")
cy.get(".panel button").contains("Skip").should("not.exist")
cy.get(".panel").should("be.visible")
cy.get(".panel button").contains("Skip").should("not.exist")
})
Then("I see the challenge panel", () => {
cy.get(".panel").should("be.visible")
cy.get(".panel").should("be.visible")
})
Then("I see a panel with only a Skip button", () => {
cy.wait(500) // This is necessary due to the animation
cy.get(".panel button").contains("Skip").should("be.visible")
cy.get(".panel ").find("button").should("have.length", 1)
cy.wait(500) // This is necessary due to the animation
cy.get(".panel button").contains("Skip").should("be.visible")
cy.get(".panel ").find("button").should("have.length", 1)
})
Then("I see a panel with only a Skip, a Cancel, and a Can't listen now button", () => {
Then(
"I see a panel with only a Skip, a Cancel, and a Can't listen now button",
() => {
cy.wait(500) // This is necessary due to the animation
cy.get(".panel button").contains("Skip").should("be.visible")
cy.get(".panel button").contains("Can't listen now").should("be.visible")
cy.get(".panel ").find("button").should("have.length", 3)
}
)
Then("I see a panel with only a Skip and a Cancel button", () => {
cy.wait(500) // This is necessary due to the animation
cy.get(".panel button").contains("Skip").should("be.visible")
cy.get(".panel button").contains("Cancel").should("be.visible")
cy.get(".panel ").find("button").should("have.length", 2)
})
Then("I see a panel with only a Skip and a Cancel button", () => {
cy.wait(500) // This is necessary due to the animation
cy.get(".panel button").contains("Skip").should("be.visible")
cy.get(".panel button").contains("Cancel").should("be.visible")
cy.get(".panel ").find("button").should("have.length", 2)
})
Then("I see a panel with only a Skip and a Cancel button", () => {
cy.wait(500) // This is necessary due to the animation
cy.get(".panel button").contains("Skip").should("be.visible")
cy.get(".panel button").contains("Cancel").should("be.visible")
cy.get(".panel ").find("button").should("have.length", 3)
cy.wait(500) // This is necessary due to the animation
cy.get(".panel button").contains("Skip").should("be.visible")
cy.get(".panel button").contains("Cancel").should("be.visible")
cy.get(".panel ").find("button").should("have.length", 3)
})

View File

@ -1,6 +1,5 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given("I see a tooltip that says {string}", text => {
cy.get(`.has-tooltip-bottom[data-tooltip="${text}"]`).should("be.visible")
Given("I see a tooltip that says {string}", (text) => {
cy.get(`.has-tooltip-bottom[data-tooltip="${text}"]`).should("be.visible")
})

View File

@ -1,6 +1,6 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I should be on {string}", url => {
const pattern = new RegExp(`${url}/?$`)
cy.url().should("match", pattern)
Then("I should be on {string}", (url) => {
const pattern = new RegExp(`${url}/?$`)
cy.url().should("match", pattern)
})

View File

@ -1,9 +1,9 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I should have progressed", () => {
cy.get("progress").should("not.have.value", 0)
cy.get("progress").should("not.have.value", 0)
})
Then("I should not have progressed", () => {
cy.get("progress").should("have.value", 0)
cy.get("progress").should("have.value", 0)
})

View File

@ -1,5 +1,5 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given("I type {string}", text => {
cy.get("input[type=text]").type(text)
Given("I type {string}", (text) => {
cy.get("input[type=text]").type(text)
})

View File

@ -1,5 +1,5 @@
import { When } from "cypress-cucumber-preprocessor/steps"
When("I wait a moment", () => {
cy.wait(1000)
cy.wait(1000)
})

View File

@ -1,5 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("{string} is graphically represented", text => {
cy.get(`img[alt="${text}"]`).should("be.visible")
Then("{string} is graphically represented", (text) => {
cy.get(`img[alt="${text}"]`).should("be.visible")
})

View File

@ -1,5 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then(/.* looks cute/, () => {
cy.get("[data-test=\"mascot-jetpack\"]").should("be.visible")
cy.get('[data-test="mascot-jetpack"]').should("be.visible")
})

View File

@ -1,11 +1,11 @@
import { Then, Given } from "cypress-cucumber-preprocessor/steps"
Given("I submit solution", () => {
cy.get("input[type=text]").type("asdfg")
cy.contains("Submit").click()
cy.get("input[type=text]").type("asdfg")
cy.contains("Submit").click()
})
Then("I'm not able to submit", () => {
cy.get("form").submit()
cy.get(".panel button").contains("Submit").should("not.exist")
cy.get("form").submit()
cy.get(".panel button").contains("Submit").should("not.exist")
})

View File

@ -1,5 +1,5 @@
import { Given } from "cypress-cucumber-preprocessor/steps"
Given("that I have an iPhone 6", () => {
cy.viewport("iphone-6")
cy.viewport("iphone-6")
})

View File

@ -1,29 +1,18 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I see a virtual keyboard with {int} keys", n => {
cy.get(".virtual-keyboard")
.find(">*")
.should("have.length", n)
Then("I see a virtual keyboard with {int} keys", (n) => {
cy.get(".virtual-keyboard").find(">*").should("have.length", n)
})
Then("the keys on the virtual keyboard have proper labels", () => {
cy.get(".virtual-keyboard")
.find(">*")
.contains("á")
.should("be.visible")
cy.get(".virtual-keyboard").find(">*").contains("á").should("be.visible")
})
Then("clicking on a key types into the input field", () => {
cy.get(".virtual-keyboard")
.find(">*")
.contains("á")
.click()
cy.get("input").should("have.value", "á")
cy.get(".virtual-keyboard").find(">*").contains("á").click()
cy.get("input").should("have.value", "á")
})
Then("the virtual keyboard is inactive", () => {
cy.get(".virtual-keyboard")
.find(">*")
.contains("á")
.should("be.disabled")
cy.get(".virtual-keyboard").find(">*").contains("á").should("be.disabled")
})

View File

@ -6,96 +6,96 @@ const COURSE_PAGE_URL = "/course/test"
const SKILL_PAGE_URL = `${COURSE_PAGE_URL}/skill/short-input-test-0?testChallenge=14fc2ae4fb35`
Before(() => {
// Reset database
cy.window().then((window) => {
cy.wrap(
window.indexedDB.deleteDatabase(`_pouch_${settings.database.local}`)
)
})
cy.visit(COURSE_PAGE_URL)
// Reset database
cy.window().then((window) => {
cy.wrap(
window.indexedDB.deleteDatabase(`_pouch_${settings.database.local}`)
)
})
cy.visit(COURSE_PAGE_URL)
})
Then("I see {int} skills that are not started", (number) => {
cy.get("[data-started=false]").should("have.length", number)
cy.get("[data-started=false]").should("have.length", number)
})
Then("I see a skill that has no image set", () => {
cy.get(".column:first-child .card img").should("have.length", 0)
cy.get(".column:first-child .card img").should("have.length", 0)
})
Then("I see 3 skills that have an image set", () => {
cy.get(".column:nth-child(2) .card img").should("have.length", 3)
cy.get(".column:nth-child(2) .card img").should("have.length", 3)
})
Then("I see 3 skills that have an image set", () => {
cy.get(".column:nth-child(2) .card img").should("have.length", 3)
cy.get(".column:nth-child(2) .card img").should("have.length", 3)
})
Given("I have a stale skill", () => {
cy.window().then((window) => {
const db = window._DB
cy.wrap(null).then(() => {
return db
.put({
_id: "skills/38c2ea1c36d2",
practiced: [
{ at: +dayjs().subtract(1, "year") },
{ at: +dayjs().subtract(1, "month") },
{ at: +dayjs().subtract(3, "month") },
{ at: +dayjs().subtract(6, "month") },
],
})
.then(() => {
cy.reload()
})
cy.window().then((window) => {
const db = window._DB
cy.wrap(null).then(() => {
return db
.put({
_id: "skills/38c2ea1c36d2",
practiced: [
{ at: +dayjs().subtract(1, "year") },
{ at: +dayjs().subtract(1, "month") },
{ at: +dayjs().subtract(3, "month") },
{ at: +dayjs().subtract(6, "month") },
],
})
.then(() => {
cy.reload()
})
})
})
})
Then("I see a stale skill", () => {
cy.get("[data-completed=true][data-stale=true]").should("have.length", 1)
cy.get(".svg-inline--fa").should("be.visible")
cy.get("[data-completed=true][data-stale=true]").should("have.length", 1)
cy.get(".svg-inline--fa").should("be.visible")
})
Then("I don't see any stale skills", () => {
cy.get("[data-completed=true][data-stale=true]").should("have.length", 0)
cy.get("[data-completed=true][data-stale=true]").should("have.length", 0)
})
Given("I complete a lesson", () => {
cy.visit(SKILL_PAGE_URL)
cy.get("input[type=text]").type("el perro")
cy.contains("Submit").click()
cy.contains("Continue").click()
cy.contains("Cancel").click()
cy.contains("Continue to course page").click()
cy.visit(SKILL_PAGE_URL)
cy.get("input[type=text]").type("el perro")
cy.contains("Submit").click()
cy.contains("Continue").click()
cy.contains("Cancel").click()
cy.contains("Continue to course page").click()
})
Then("I'm redirected to the course page", () => {
cy.url().should("match", new RegExp(`.*${COURSE_PAGE_URL}/?$`))
cy.url().should("match", new RegExp(`.*${COURSE_PAGE_URL}/?$`))
})
Then("I see a completed skill", () => {
cy.get("[data-completed=true][data-stale=false]").should("have.length", 1)
cy.get(".svg-inline--fa").should("be.visible")
cy.get("[data-completed=true][data-stale=false]").should("have.length", 1)
cy.get(".svg-inline--fa").should("be.visible")
})
Then("I see a started skill", () => {
cy.get("[data-started=true][data-stale=false]").should("have.length", 1)
cy.get("[data-started=true][data-stale=false]").should("have.length", 1)
})
And("I see a skill with 20% progress", () => {
cy.get("[data-test='skill card'] progress").should("have.value", 0.2)
cy.get("[data-test='skill card'] progress").should("have.attr", "max", "1")
cy.get("[data-test='skill card'] progress").should("have.value", 0.2)
cy.get("[data-test='skill card'] progress").should("have.attr", "max", "1")
})
Given("practice statistics are saved correctly", () => {
cy.window().then((window) => {
const db = window._DB
cy.wrap(null).then(() => {
return db.get("skills/e70976d68b28").then((doc) => {
cy.wrap(doc.practiced[0].incorrect).should("equal", 0)
cy.wrap(doc.practiced[0].correct).should("equal", 1)
})
})
cy.window().then((window) => {
const db = window._DB
cy.wrap(null).then(() => {
return db.get("skills/e70976d68b28").then((doc) => {
cy.wrap(doc.practiced[0].incorrect).should("equal", 0)
cy.wrap(doc.practiced[0].correct).should("equal", 1)
})
})
})
})

View File

@ -1,13 +1,13 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("my credentials are incorrect", () => {
cy.window().then((window) => {
window._test_credentials_correct = false
})
cy.window().then((window) => {
window._test_credentials_correct = false
})
})
Then("my credentials are correct", () => {
cy.window().then((window) => {
window._test_credentials_correct = true
})
cy.window().then((window) => {
window._test_credentials_correct = true
})
})

View File

@ -1,41 +1,33 @@
import { Then, Given } from "cypress-cucumber-preprocessor/steps"
Then("I see 3 options", () => {
cy.get(".options")
.find(".option:visible")
.should("have.length", 3)
cy.get(".options").find(".option:visible").should("have.length", 3)
})
Then("I see 2 inactive options", () => {
cy.get(".options")
.find(".option[data-test=inactive]:visible")
.should("have.length", 2)
cy.get(".options")
.find(".option[data-test=inactive]:visible")
.should("have.length", 2)
})
Then("I see 1 active option", () => {
cy.get(".options")
.find(".option[data-test=active]")
.should("have.length", 1)
cy.get(".options").find(".option[data-test=active]").should("have.length", 1)
})
Then("every option is inactive", () => {
cy.get(".options")
.find(".option[data-test=neutral]:visible")
.should("have.length", 3)
cy.get(".options")
.find(".option[data-test=neutral]:visible")
.should("have.length", 3)
})
Given("I select an option", () => {
cy.get(".option:visible")
.first()
.click()
cy.get(".option:visible").first().click()
})
Then("I select the correct option", () => {
cy.get(".option[data-test-correct=true]").click()
cy.get(".option[data-test-correct=true]").click()
})
Then("I select an incorrect option", () => {
cy.get(".option[data-test-correct=false]")
.first()
.click()
cy.get(".option[data-test-correct=false]").first().click()
})

View File

@ -1,7 +1,7 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I am not really calling the registration API", () => {
cy.window().then((window) => {
window._test_fake_signup = true
})
cy.window().then((window) => {
window._test_fake_signup = true
})
})

View File

@ -1,5 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps"
Then("I don't see any completed skills", () => {
cy.get("[data-completed=true][data-stale=false]").should("have.length", 0)
cy.get("[data-completed=true][data-stale=false]").should("have.length", 0)
})

View File

@ -16,8 +16,8 @@ const cucumber = require("cypress-cucumber-preprocessor").default
const { renameSync } = require("fs")
module.exports = (on) => {
on("file:preprocessor", cucumber())
on("after:screenshot", ({ path }) => {
renameSync(path, path.replace(/ \(\d*\)/i, ""))
})
on("file:preprocessor", cucumber())
on("after:screenshot", ({ path }) => {
renameSync(path, path.replace(/ \(\d*\)/i, ""))
})
}

View File

@ -21,5 +21,5 @@ import "@percy/cypress"
// require('./commands')
Cypress.on("window:before:load", (win) => {
win.isCypress = true
win.isCypress = true
})

View File

@ -17,17 +17,7 @@
"fetchAudios": "./scripts/fetchAudios.sh",
"percypress": "bash ./percypress.sh",
"test": "yarn dev & yarn percypress",
"test:ci": "npx serve __sapper__/export -l 3000 & yarn percypress",
"prettierfix": "yarn prettierfix:src && yarn prettierfix:cypress && yarn prettierfix:svelte",
"prettierfix:src": "prettier --write --plugin-search-dir=. ./src/**/*.js",
"prettierfix:cypress": "prettier --write --plugin-search-dir=. ./cypress/**/*.js",
"prettierfix:svelte": "prettier --write --plugin-search-dir=. ./src/**/*.svelte",
"eslintfix": "eslint ./src --fix && eslint ./cypress --fix",
"prettiercheck": "yarn prettiercheck:src && yarn prettiercheck:cypress && yarn prettiercheck:svelte",
"prettiercheck:src": "prettier --plugin-search-dir=. ./src/**/*.js",
"prettiercheck:cypress": "prettier --plugin-search-dir=. ./cypress/**/*.js",
"prettiercheck:svelte": "prettier --plugin-search-dir=. ./src/**/*.svelte",
"eslintcheck": "eslint ./src && eslint ./cypress"
"test:ci": "npx serve __sapper__/export -l 3000 & yarn percypress"
},
"publishConfig": {
"access": "public"
@ -93,8 +83,6 @@
"node-sass": "5.0.0",
"npm-run-all": "4.1.5",
"nyc": "15.1.0",
"prettier": "2.5.1",
"prettier-plugin-svelte": "2.6.0",
"raw-loader": "4.0.2",
"sapper": "0.29.3",
"sass-loader": "10.2.1",

View File

@ -5,44 +5,44 @@ jest.mock("lodash.shuffle")
import shuffle from "lodash.shuffle"
describe("prepareChallenge", () => {
it("returns correct value", () => {
shuffle.mockReturnValue([])
const currentChallenge = { foo: "bar" }
const alternativeChallenges = []
expect(
prepareChallenge({ currentChallenge, alternativeChallenges })
).toEqual([])
})
it("returns correct value", () => {
shuffle.mockReturnValue([])
const currentChallenge = { foo: "bar" }
const alternativeChallenges = []
expect(
prepareChallenge({ currentChallenge, alternativeChallenges })
).toEqual([])
})
it("uses only card challenges", () => {
shuffle.mockImplementation(jest.requireActual("lodash.shuffle"))
const currentChallenge = {
id: 4,
type: "cards",
pictures: ["lion1.jpg", "lion2.jpg", "lion3.jpg"],
meaningInSourceLanguage: "lion",
formInTargetLanguage: "león",
}
const alternativeChallenges = [
{
id: 202,
type: "cards",
pictures: ["bear1.jpg", "bear2.jpg", "bear3.jpg"],
meaningInSourceLanguage: "bear",
formInTargetLanguage: "oso",
},
{
id: 6663,
type: "shortInput",
meaningInSourceLanguage: "dog",
formInTargetLanguage: ["perro", "el perro", "can"],
},
]
expect(
prepareChallenge({
currentChallenge,
alternativeChallenges,
}).filter(({ type }) => type !== "cards")
).toEqual([])
})
it("uses only card challenges", () => {
shuffle.mockImplementation(jest.requireActual("lodash.shuffle"))
const currentChallenge = {
id: 4,
type: "cards",
pictures: ["lion1.jpg", "lion2.jpg", "lion3.jpg"],
meaningInSourceLanguage: "lion",
formInTargetLanguage: "león",
}
const alternativeChallenges = [
{
id: 202,
type: "cards",
pictures: ["bear1.jpg", "bear2.jpg", "bear3.jpg"],
meaningInSourceLanguage: "bear",
formInTargetLanguage: "oso",
},
{
id: 6663,
type: "shortInput",
meaningInSourceLanguage: "dog",
formInTargetLanguage: ["perro", "el perro", "can"],
},
]
expect(
prepareChallenge({
currentChallenge,
alternativeChallenges,
}).filter(({ type }) => type !== "cards")
).toEqual([])
})
})

View File

@ -1,9 +1,9 @@
import { writable } from "svelte/store"
const authStore = writable({
user: null,
online: null,
dbUpdatedAt: null,
user: null,
online: null,
dbUpdatedAt: null,
})
export default authStore

View File

@ -3,15 +3,15 @@ import "./mystyles.scss"
import "./i18n"
import { library, dom } from "@fortawesome/fontawesome-svg-core"
import {
faVolumeUp,
faCheckSquare,
faDumbbell,
faStar,
faUser,
faLock,
faEnvelope,
faHeart,
faSpinner
faVolumeUp,
faCheckSquare,
faDumbbell,
faStar,
faUser,
faLock,
faEnvelope,
faHeart,
faSpinner,
} from "@fortawesome/free-solid-svg-icons"
import { faTwitter } from "@fortawesome/free-brands-svg-icons"
@ -28,10 +28,9 @@ library.add(faSpinner)
dom.watch()
sapper.start({
target: document.querySelector("#sapper"),
target: document.querySelector("#sapper"),
})
if (!window.isCypress) {
require("@openfonts/noto-sans_all")
require("@openfonts/noto-sans_all")
}

View File

@ -231,7 +231,6 @@
<div class="container">
<FanfareScreen
courseURL="{courseURL}"
rawChallenges="{rawChallenges}"
skillId="{skillId}"
stats="{stats}"
/>

View File

@ -1,27 +1,26 @@
const getActualParent = (node: HTMLElement): HTMLElement => {
if (node?.parentElement?.id) {
return node.parentElement
}
if (node?.parentElement?.parentElement) {
return node.parentElement.parentElement
}
if (node?.parentElement?.id) {
return node.parentElement
}
throw new Error("Invalid <Chip />")
if (node?.parentElement?.parentElement) {
return node.parentElement.parentElement
}
throw new Error("Invalid <Chip />")
}
export const getNodeType = (node: HTMLElement): string => getActualParent(node).id
export const getNodeType = (node: HTMLElement): string =>
getActualParent(node).id
export const getChipIndex = (node: HTMLElement): number => {
if (!node.classList.contains("chip") && node.parentElement) {
return getChipIndex(node.parentElement)
}
if (!node.classList.contains("chip") && node.parentElement) {
return getChipIndex(node.parentElement)
}
if (node.previousSibling !== null) {
return 1 + getChipIndex(node.previousSibling as HTMLElement)
}
if (node.previousSibling !== null) {
return 1 + getChipIndex(node.previousSibling as HTMLElement)
}
return 0
return 0
}

View File

@ -1,14 +1,17 @@
import type { Writable } from "svelte/store"
import Sortable from "sortablejs"
export const createSortable = (element: HTMLElement, store: Writable<string[]>): Sortable => {
return Sortable.create(element, {
group: "chips",
store: {
get: () => [],
set: function(sortable) {
store.set(sortable.toArray())
}
}
})
export const createSortable = (
element: HTMLElement,
store: Writable<string[]>
): Sortable => {
return Sortable.create(element, {
group: "chips",
store: {
get: () => [],
set: function (sortable) {
store.set(sortable.toArray())
},
},
})
}

View File

@ -13,7 +13,6 @@
import Columns from "lluis/Columns.svelte"
import Title from "lluis/Title.svelte"
export let rawChallenges
export let courseURL
export let skillId
export let stats

View File

@ -1,19 +1,23 @@
import { get_course, get_skill_data, get_skill_introduction } from "../index"
describe("get_course", () => {
it("returns correct course data", async () => {
expect(await get_course({ courseName: "test" })).toMatchSnapshot()
})
it("returns correct course data", async () => {
expect(await get_course({ courseName: "test" })).toMatchSnapshot()
})
})
describe("get_skill_data", () => {
it("returns correct course data", async () => {
expect(await get_skill_data({ courseName: "test", skillName: "animals" })).toMatchSnapshot()
})
it("returns correct course data", async () => {
expect(
await get_skill_data({ courseName: "test", skillName: "animals" })
).toMatchSnapshot()
})
})
describe("get_skill_introduction", () => {
it("returns correct course data", async () => {
expect(await get_skill_introduction({ courseName: "test", skillName: "animals" })).toMatchSnapshot()
})
it("returns correct course data", async () => {
expect(
await get_skill_introduction({ courseName: "test", skillName: "animals" })
).toMatchSnapshot()
})
})

View File

@ -1,83 +1,103 @@
import loadMarkdownModule from "../utils/loadMarkdownModule"
export type SkillDataType = {
id: string;
practiceHref: string;
title: string;
levels: number;
introduction: string;
summary: string[];
id: string
practiceHref: string
title: string
levels: number
introduction: string
summary: string[]
}
export type ModuleDataType = {
title: string;
skills: SkillDataType[];
title: string
skills: SkillDataType[]
}
export type CourseDataType = {
courseName: string;
modules: ModuleDataType[];
languageName: string;
repositoryURL: string;
languageCode: string;
specialCharacters: string[];
courseName: string
modules: ModuleDataType[]
languageName: string
repositoryURL: string
languageCode: string
specialCharacters: string[]
}
export const get_course = async ({ courseName }: { courseName: string }): Promise<CourseDataType> => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { modules, languageName, repositoryURL, languageCode, specialCharacters } = require(
`../courses/${courseName}/courseData.json`
)
export const get_course = async ({
courseName,
}: {
courseName: string
}): Promise<CourseDataType> => {
const {
modules,
languageName,
repositoryURL,
languageCode,
specialCharacters,
} = require(`../courses/${courseName}/courseData.json`) // eslint-disable-line @typescript-eslint/no-var-requires
return { courseName, modules, languageName, repositoryURL, languageCode, specialCharacters }
return {
courseName,
modules,
languageName,
repositoryURL,
languageCode,
specialCharacters,
}
}
export const get_skill_data = async ({ courseName, skillName }: { courseName: string, skillName: string }) => {
const {
languageName,
languageCode,
specialCharacters,
repositoryURL,
} = await get_course({ courseName })
// eslint-disable-next-line @typescript-eslint/no-var-requires
const skillData = require(
`../courses/${courseName}/challenges/${skillName}.json`
)
const rawChallenges = skillData.challenges
const challengesPerLevel = skillData.challenges.length / skillData.levels
export const get_skill_data = async ({
courseName,
skillName,
}: {
courseName: string
skillName: string
}) => {
const { languageName, languageCode, specialCharacters, repositoryURL } =
await get_course({ courseName })
// eslint-disable-next-line @typescript-eslint/no-var-requires
const skillData = require(`../courses/${courseName}/challenges/${skillName}.json`)
const rawChallenges = skillData.challenges
const challengesPerLevel = skillData.challenges.length / skillData.levels
const skillId = skillData.id
const skillId = skillData.id
return {
rawChallenges: Array.from(rawChallenges),
languageName,
languageCode,
specialCharacters,
repositoryURL,
skillName,
skillId,
challengesPerLevel,
courseURL: `/course/${courseName}`,
}
return {
rawChallenges: Array.from(rawChallenges),
languageName,
languageCode,
specialCharacters,
repositoryURL,
skillName,
skillId,
challengesPerLevel,
courseURL: `/course/${courseName}`,
}
}
export const get_skill_introduction = async ({ courseName, skillName }: { courseName: string, skillName: string })=> {
const { modules } = await get_course({ courseName })
export const get_skill_introduction = async ({
courseName,
skillName,
}: {
courseName: string
skillName: string
}) => {
const { modules } = await get_course({ courseName })
for (const module of modules) {
for (const skill of module.skills) {
if (skill.practiceHref === skillName) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const markdownModule = require(`../courses/${courseName}/introduction/${skill.introduction}`)
for (const module of modules) {
for (const skill of module.skills) {
if (skill.practiceHref === skillName) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const markdownModule = require(`../courses/${courseName}/introduction/${skill.introduction}`)
return {
skillName,
courseName,
title: skill.title,
practiceHref: skill.practiceHref,
readmeHTML: loadMarkdownModule(markdownModule)
}
}
return {
skillName,
courseName,
title: skill.title,
practiceHref: skill.practiceHref,
readmeHTML: loadMarkdownModule(markdownModule),
}
}
}
}
}

View File

@ -9,174 +9,174 @@ let remoteDB
let syncHandler
const createLocalPouchDb = (dbName) => {
const PouchDB =
const PouchDB =
process.env.JEST_WORKER_ID !== undefined
? require("pouchdb")
: require("pouchdb").default
const newDb = new PouchDB(dbName).setMaxListeners(
settings.database.maxNumberOfListeners
)
? require("pouchdb")
: require("pouchdb").default
const newDb = new PouchDB(dbName).setMaxListeners(
settings.database.maxNumberOfListeners
)
newDb
.changes({
since: "now",
live: true,
include_docs: true,
})
.on("change", () => {
if (process.env.JEST_WORKER_ID !== undefined) {
return
}
const authStore = require("../auth").default
authStore.update((value) => ({
...value,
dbUpdatedAt: Date.now(),
}))
})
newDb
.changes({
since: "now",
live: true,
include_docs: true,
})
.on("change", () => {
if (process.env.JEST_WORKER_ID !== undefined) {
return
}
const authStore = require("../auth").default
authStore.update((value) => ({
...value,
dbUpdatedAt: Date.now(),
}))
})
return newDb
return newDb
}
if (isBrowser() === true) {
const authStore = require("../auth").default
const PouchDB = require("pouchdb").default
const authStore = require("../auth").default
const PouchDB = require("pouchdb").default
// Connect to remote database
remoteDB = new PouchDB(
`${settings.database.remote}/${Cookies.get("loginDb")}`,
{ skip_setup: true, live: true }
)
// Connect to remote database
remoteDB = new PouchDB(
`${settings.database.remote}/${Cookies.get("loginDb")}`,
{ skip_setup: true, live: true }
)
// Connect to local database
db = createLocalPouchDb(settings.database.local)
window._DB = db
// Connect to local database
db = createLocalPouchDb(settings.database.local)
window._DB = db
// Detect fake user session
if (Cookies.get("loginDb") === getUserDbName("JohnDoe")) {
// Detect fake user session
if (Cookies.get("loginDb") === getUserDbName("JohnDoe")) {
authStore.update((value) => ({
...value,
user: { name: "JohnDoe" },
online: true,
}))
}
// Detect existing user session
if (Cookies.get("loginDb") && settings.features.authEnabled) {
fetch(`${settings.database.remote}/_session`, { credentials: "include" })
.then((data) => data.json())
.then((user) => {
if (user.userCtx.name === null) {
return
}
authStore.update((value) => ({
...value,
user: { name: "JohnDoe" },
online: true,
...value,
user: { name: user.userCtx.name },
}))
}
// Detect existing user session
if (Cookies.get("loginDb") && settings.features.authEnabled) {
fetch(`${settings.database.remote}/_session`, { credentials: "include" })
.then((data) => data.json())
.then((user) => {
if (user.userCtx.name === null) {
return
}
authStore.update((value) => ({
...value,
user: { name: user.userCtx.name },
}))
startSync()
})
} else {
startSync()
})
} else {
// Without a sessios, there is no sync
authStore.update((value) => ({
...value,
online: false,
}))
authStore.update((value) => ({
...value,
online: false,
}))
}
// Fake login for testing purposes
window._fakeLogin = () => {
Cookies.set("loginDb", getUserDbName("JohnDoe"), {
expires: settings.database.auth.expireDays,
})
window.location.href = "/"
}
// Add login function
window._Login = async (username, password) => {
if (window._test_credentials_correct === false) {
throw new Error("Incorrect username or password")
}
// Fake login for testing purposes
window._fakeLogin = () => {
Cookies.set("loginDb", getUserDbName("JohnDoe"), {
expires: settings.database.auth.expireDays,
if (window._test_credentials_correct === true) {
return window._fakeLogin()
}
const response = await (
await fetch(`${settings.database.remote}/_session`, {
method: "post",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
})
).json()
if (response.error) {
if (response.error === "unauthorized") {
throw new Error("Username or password is incorrect")
}
throw new Error("Couldn't log in. Please try again later")
}
authStore.update((value) => ({
...value,
online: null,
}))
Cookies.set("loginDb", getUserDbName(username), {
expires: settings.database.auth.expireDays,
})
window.location.reload(false)
window.location.href = ""
}
// Logout
window._Logout = async () => {
try {
if (syncHandler) {
await syncHandler.cancel()
await fetch(`${settings.database.remote}/_session`, {
method: "delete",
})
window.location.href = "/"
}
} finally {
Cookies.remove("loginDb")
authStore.update((value) => ({
...value,
user: null,
online: null,
}))
await db.destroy()
window.location.reload(false)
}
}
// Add login function
window._Login = async (username, password) => {
if (window._test_credentials_correct === false) {
throw new Error("Incorrect username or password")
}
if (window._test_credentials_correct === true) {
return window._fakeLogin()
}
const response = await (
await fetch(`${settings.database.remote}/_session`, {
method: "post",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
password,
}),
})
).json()
if (response.error) {
if (response.error === "unauthorized") {
throw new Error("Username or password is incorrect")
}
throw new Error("Couldn't log in. Please try again later")
}
authStore.update((value) => ({
...value,
online: null,
}))
Cookies.set("loginDb", getUserDbName(username), {
expires: settings.database.auth.expireDays,
})
window.location.reload(false)
window.location.href = ""
}
// Logout
window._Logout = async () => {
try {
if (syncHandler) {
await syncHandler.cancel()
await fetch(`${settings.database.remote}/_session`, {
method: "delete",
})
}
} finally {
Cookies.remove("loginDb")
authStore.update((value) => ({
...value,
user: null,
online: null,
}))
await db.destroy()
window.location.reload(false)
}
}
// Keep databases in sync
const startSync = () => {
syncHandler = db
.sync(remoteDB)
.on("complete", function () {
authStore.update((value) => ({ ...value, online: true }))
})
.on("error", function () {
authStore.update((value) => ({ ...value, online: false }))
})
}
// Keep databases in sync
const startSync = () => {
syncHandler = db
.sync(remoteDB)
.on("complete", function () {
authStore.update((value) => ({ ...value, online: true }))
})
.on("error", function () {
authStore.update((value) => ({ ...value, online: false }))
})
}
}
if (process.env.JEST_WORKER_ID !== undefined) {
// This is a test database for Jest tests that can reset itself
db = createLocalPouchDb(settings.database.local)
db.__reset = async () => {
const allDocs = await db.allDocs()
await Promise.all(
allDocs.rows.map(function (row) {
return db.remove(row.id, row.value.rev)
})
)
}
// This is a test database for Jest tests that can reset itself
db = createLocalPouchDb(settings.database.local)
db.__reset = async () => {
const allDocs = await db.allDocs()
await Promise.all(
allDocs.rows.map(function (row) {
return db.remove(row.id, row.value.rev)
})
)
}
}
export default db

View File

@ -1,11 +1,11 @@
import getUserDbName from "./getUserDbName"
describe("getUserDbName", () => {
it("returns correct result 1", () => {
expect(getUserDbName("jan")).toEqual("userdb-6a616e")
})
it("returns correct result 1", () => {
expect(getUserDbName("jan")).toEqual("userdb-6a616e")
})
it("returns correct result 2", () => {
expect(getUserDbName("januska")).toEqual("userdb-6a616e75736b61")
})
it("returns correct result 2", () => {
expect(getUserDbName("januska")).toEqual("userdb-6a616e75736b61")
})
})

View File

@ -1,7 +1,7 @@
const hashUsername = (username) =>
username
.split("")
.map((c) => c.charCodeAt(0).toString(16))
.join("")
username
.split("")
.map((c) => c.charCodeAt(0).toString(16))
.join("")
export default (username) => `userdb-${hashUsername(username)}`

View File

@ -2,10 +2,10 @@ import isBrowser from "../utils/isBrowser"
import db from "./db"
export default (listener) => {
listener(db)
if (isBrowser() === true && process.env.JEST_WORKER_ID === undefined) {
listener(db)
if (isBrowser() === true && process.env.JEST_WORKER_ID === undefined) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const authStore = require("../auth").default
authStore.subscribe(() => listener(db))
}
const authStore = require("../auth").default
authStore.subscribe(() => listener(db))
}
}

View File

@ -2,47 +2,47 @@ import db from "../../db"
import savePractice from "../savePractice"
describe("db/skill/savePractice", () => {
beforeEach(async () => {
await db.__reset()
})
beforeEach(async () => {
await db.__reset()
})
it("correctly creates new data", async () => {
await savePractice(db, { id: "fooBar", correct: 5, incorrect: 3 })
expect(await db.get("skills/fooBar")).toEqual({
_id: "skills/fooBar",
_rev: expect.anything(),
practiced: [
{ at: expect.anything(), correct: 5, incorrect: 3, skipped: 0 },
],
})
it("correctly creates new data", async () => {
await savePractice(db, { id: "fooBar", correct: 5, incorrect: 3 })
expect(await db.get("skills/fooBar")).toEqual({
_id: "skills/fooBar",
_rev: expect.anything(),
practiced: [
{ at: expect.anything(), correct: 5, incorrect: 3, skipped: 0 },
],
})
})
it("correctly creates new data - with skipped", async () => {
await savePractice(db, {
id: "fooBar",
correct: 5,
incorrect: 3,
skipped: 1,
})
expect(await db.get("skills/fooBar")).toEqual({
_id: "skills/fooBar",
_rev: expect.anything(),
practiced: [
{ at: expect.anything(), correct: 5, incorrect: 3, skipped: 1 },
],
})
it("correctly creates new data - with skipped", async () => {
await savePractice(db, {
id: "fooBar",
correct: 5,
incorrect: 3,
skipped: 1,
})
expect(await db.get("skills/fooBar")).toEqual({
_id: "skills/fooBar",
_rev: expect.anything(),
practiced: [
{ at: expect.anything(), correct: 5, incorrect: 3, skipped: 1 },
],
})
})
it("correctly updates data", async () => {
await savePractice(db, { id: "fooBar", correct: 5, incorrect: 10 })
await savePractice(db, { id: "fooBar", correct: 6, incorrect: 3 })
expect(await db.get("skills/fooBar")).toEqual({
_id: "skills/fooBar",
_rev: expect.anything(),
practiced: [
{ at: expect.anything(), correct: 5, incorrect: 10, skipped: 0 },
{ at: expect.anything(), correct: 6, incorrect: 3, skipped: 0 },
],
})
it("correctly updates data", async () => {
await savePractice(db, { id: "fooBar", correct: 5, incorrect: 10 })
await savePractice(db, { id: "fooBar", correct: 6, incorrect: 3 })
expect(await db.get("skills/fooBar")).toEqual({
_id: "skills/fooBar",
_rev: expect.anything(),
practiced: [
{ at: expect.anything(), correct: 5, incorrect: 10, skipped: 0 },
{ at: expect.anything(), correct: 6, incorrect: 3, skipped: 0 },
],
})
})
})

View File

@ -1,24 +1,24 @@
import dayjs from "dayjs"
const fib = n => (n > 1 ? fib(n - 1) + fib(n - 2) : 1)
const fib = (n) => (n > 1 ? fib(n - 1) + fib(n - 2) : 1)
export const daysUntilNextPractice = ({ practicesSoFar }) => fib(practicesSoFar)
export const getLastPractice = practices =>
[...practices].sort((a, b) => (dayjs(a.at).isAfter(dayjs(b.at)) ? -1 : 1))[0]
.at
export const getLastPractice = (practices) =>
[...practices].sort((a, b) => (dayjs(a.at).isAfter(dayjs(b.at)) ? -1 : 1))[0]
.at
export const wouldBeStale = ({ lastPractice, practicesSoFar }) => {
const shouldBeStaleAt = dayjs(lastPractice).add(
daysUntilNextPractice({ practicesSoFar }),
"day"
)
const shouldBeStaleAt = dayjs(lastPractice).add(
daysUntilNextPractice({ practicesSoFar }),
"day"
)
return dayjs().isAfter(dayjs(dayjs(shouldBeStaleAt).subtract(1, "second")))
return dayjs().isAfter(dayjs(dayjs(shouldBeStaleAt).subtract(1, "second")))
}
export const isStale = ({ practices }) =>
wouldBeStale({
practicesSoFar: practices.length,
lastPractice: getLastPractice(practices)
})
wouldBeStale({
practicesSoFar: practices.length,
lastPractice: getLastPractice(practices),
})

View File

@ -1,9 +1,9 @@
import dayjs from "dayjs"
import {
daysUntilNextPractice,
wouldBeStale,
getLastPractice,
isStale
daysUntilNextPractice,
wouldBeStale,
getLastPractice,
isStale,
} from "./_logic"
const today = dayjs()
@ -11,98 +11,98 @@ const yesterday = dayjs().subtract(1, "day")
const dayBeforeYesterday = dayjs().subtract(2, "day")
describe("daysUntilNextPractice", () => {
const tests = [
{ practicesSoFar: 1, expectedOutput: 1 },
{ practicesSoFar: 2, expectedOutput: 2 },
{ practicesSoFar: 3, expectedOutput: 3 },
{ practicesSoFar: 4, expectedOutput: 5 }
]
const tests = [
{ practicesSoFar: 1, expectedOutput: 1 },
{ practicesSoFar: 2, expectedOutput: 2 },
{ practicesSoFar: 3, expectedOutput: 3 },
{ practicesSoFar: 4, expectedOutput: 5 },
]
tests.forEach(({ practicesSoFar, expectedOutput }) => {
it(`returns ${expectedOutput} for ${practicesSoFar}`, () => {
expect(daysUntilNextPractice({ practicesSoFar })).toEqual(expectedOutput)
})
tests.forEach(({ practicesSoFar, expectedOutput }) => {
it(`returns ${expectedOutput} for ${practicesSoFar}`, () => {
expect(daysUntilNextPractice({ practicesSoFar })).toEqual(expectedOutput)
})
})
})
describe("wouldBeStale", () => {
const tests = [
{
description: "expired after first practice",
lastPractice: yesterday,
practicesSoFar: 1,
expectedOutput: true
},
{
description: "not expired after second practice",
lastPractice: yesterday,
practicesSoFar: 2,
expectedOutput: false
},
{
description: "expired after second practice",
lastPractice: dayBeforeYesterday,
practicesSoFar: 2,
expectedOutput: true
}
]
const tests = [
{
description: "expired after first practice",
lastPractice: yesterday,
practicesSoFar: 1,
expectedOutput: true,
},
{
description: "not expired after second practice",
lastPractice: yesterday,
practicesSoFar: 2,
expectedOutput: false,
},
{
description: "expired after second practice",
lastPractice: dayBeforeYesterday,
practicesSoFar: 2,
expectedOutput: true,
},
]
tests.forEach(
({ lastPractice, practicesSoFar, expectedOutput, description }) => {
it(`returns correct value when ${description}`, () => {
expect(wouldBeStale({ practicesSoFar, lastPractice })).toEqual(
expectedOutput
)
})
}
)
tests.forEach(
({ lastPractice, practicesSoFar, expectedOutput, description }) => {
it(`returns correct value when ${description}`, () => {
expect(wouldBeStale({ practicesSoFar, lastPractice })).toEqual(
expectedOutput
)
})
}
)
})
describe("getLastPractice", () => {
const tests = [
{
practices: [{ at: dayBeforeYesterday }],
expectedOutput: dayBeforeYesterday
},
{
practices: [{ at: dayBeforeYesterday }, { at: yesterday }],
expectedOutput: yesterday
},
{
practices: [{ at: yesterday }, { at: today }, { at: dayBeforeYesterday }],
expectedOutput: today
}
]
const tests = [
{
practices: [{ at: dayBeforeYesterday }],
expectedOutput: dayBeforeYesterday,
},
{
practices: [{ at: dayBeforeYesterday }, { at: yesterday }],
expectedOutput: yesterday,
},
{
practices: [{ at: yesterday }, { at: today }, { at: dayBeforeYesterday }],
expectedOutput: today,
},
]
tests.forEach(({ practices, expectedOutput }) => {
it(`returns correct value for ${practices.length} items`, () => {
expect(getLastPractice(practices)).toEqual(expectedOutput)
})
tests.forEach(({ practices, expectedOutput }) => {
it(`returns correct value for ${practices.length} items`, () => {
expect(getLastPractice(practices)).toEqual(expectedOutput)
})
})
})
describe("isStale", () => {
const tests = [
{
description: "expired after first practice",
practices: [{ at: +dayBeforeYesterday }],
expectedOutput: true
},
{
description: "not expired after second practice",
practices: [{ at: +dayBeforeYesterday }, { at: +yesterday }],
expectedOutput: false
},
{
description: "not expired after second practice",
practices: [{ at: +dayBeforeYesterday }, { at: +dayBeforeYesterday }],
expectedOutput: true
}
]
const tests = [
{
description: "expired after first practice",
practices: [{ at: +dayBeforeYesterday }],
expectedOutput: true,
},
{
description: "not expired after second practice",
practices: [{ at: +dayBeforeYesterday }, { at: +yesterday }],
expectedOutput: false,
},
{
description: "not expired after second practice",
practices: [{ at: +dayBeforeYesterday }, { at: +dayBeforeYesterday }],
expectedOutput: true,
},
]
tests.forEach(({ practices, expectedOutput, description }) => {
it(`returns correct value when ${description}`, () => {
expect(isStale({ practices })).toEqual(expectedOutput)
})
tests.forEach(({ practices, expectedOutput, description }) => {
it(`returns correct value when ${description}`, () => {
expect(isStale({ practices })).toEqual(expectedOutput)
})
})
})

View File

@ -1,30 +1,30 @@
import { isStale } from "./_logic"
export default async (db, { id }) => {
if (!db) return null
if (!db) return null
try {
const { practiced } = await db.get(`skills/${id}`)
const validPractices = practiced.filter(
({ correct }) => correct === undefined || correct > 0
)
try {
const { practiced } = await db.get(`skills/${id}`)
const validPractices = practiced.filter(
({ correct }) => correct === undefined || correct > 0
)
if (validPractices.length === 0) {
return { started: false, stale: null, progress: 0 }
}
const progress = validPractices.reduce(
(acc, { correct, skipped }) =>
acc + (correct || 1) / ((correct || 1) + (skipped || 0)),
0
)
return {
started: validPractices.length >= 1,
stale: isStale({ practices: practiced }),
progress,
}
} catch {
return { started: false, stale: null, progress: 0 }
if (validPractices.length === 0) {
return { started: false, stale: null, progress: 0 }
}
const progress = validPractices.reduce(
(acc, { correct, skipped }) =>
acc + (correct || 1) / ((correct || 1) + (skipped || 0)),
0
)
return {
started: validPractices.length >= 1,
stale: isStale({ practices: practiced }),
progress,
}
} catch {
return { started: false, stale: null, progress: 0 }
}
}

View File

@ -2,104 +2,104 @@ import dayjs from "dayjs"
import getSkillStats from "./getSkillStats"
const createFakeDb = (resolvedValue) => ({
get: () => new Promise((resolve) => resolve(resolvedValue)),
get: () => new Promise((resolve) => resolve(resolvedValue)),
})
const createFailingFakeDb = () => ({
get: () => new Promise((_, reject) => reject()),
get: () => new Promise((_, reject) => reject()),
})
describe("getSkillStats", function () {
it("return correct value - skill cannot be found", async () => {
const fakeDb = createFailingFakeDb()
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: false,
progress: 0,
stale: null,
})
it("return correct value - skill cannot be found", async () => {
const fakeDb = createFailingFakeDb()
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: false,
progress: 0,
stale: null,
})
})
it("return correct value - never practiced", async () => {
const fakeDb = createFakeDb({ practiced: [] })
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: false,
progress: 0,
stale: null,
})
it("return correct value - never practiced", async () => {
const fakeDb = createFakeDb({ practiced: [] })
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: false,
progress: 0,
stale: null,
})
})
it("return correct value - practiced recently", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: new Date().valueOf(),
correct: 1,
incorrect: 0,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: true,
stale: false,
progress: 1,
})
it("return correct value - practiced recently", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: new Date().valueOf(),
correct: 1,
incorrect: 0,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: true,
stale: false,
progress: 1,
})
})
it("return correct value - practiced recently, partially skipped", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: new Date().valueOf(),
correct: 2,
incorrect: 0,
},
{
at: new Date().valueOf(),
correct: 1,
skipped: 1,
incorrect: 0,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: true,
stale: false,
progress: 1.5,
})
it("return correct value - practiced recently, partially skipped", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: new Date().valueOf(),
correct: 2,
incorrect: 0,
},
{
at: new Date().valueOf(),
correct: 1,
skipped: 1,
incorrect: 0,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: true,
stale: false,
progress: 1.5,
})
})
it("return correct value - practiced recently, all skipped", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: new Date().valueOf(),
correct: 0,
incorrect: 0,
skipped: 1,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: false,
stale: null,
progress: 0,
})
it("return correct value - practiced recently, all skipped", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: new Date().valueOf(),
correct: 0,
incorrect: 0,
skipped: 1,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: false,
stale: null,
progress: 0,
})
})
it("return correct value - practiced a long time ago", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: dayjs(new Date().valueOf()).subtract(5, "day"),
correct: 1,
incorrect: 0,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: true,
stale: true,
progress: 1,
})
it("return correct value - practiced a long time ago", async () => {
const fakeDb = createFakeDb({
practiced: [
{
at: dayjs(new Date().valueOf()).subtract(5, "day"),
correct: 1,
incorrect: 0,
},
],
})
expect(await getSkillStats(fakeDb, { id: "foo" })).toEqual({
started: true,
stale: true,
progress: 1,
})
})
})

View File

@ -1,27 +1,27 @@
const getCurrentData = async (db, _id) => {
try {
return await db.get(_id)
} catch {
return {
_id,
practiced: [],
}
try {
return await db.get(_id)
} catch {
return {
_id,
practiced: [],
}
}
}
export default async (db, { id, correct, incorrect, skipped }) => {
const doc = await getCurrentData(db, `skills/${id}`)
const doc = await getCurrentData(db, `skills/${id}`)
await db.put({
...doc,
practiced: [
...doc.practiced,
{
at: new Date().valueOf(),
correct,
incorrect,
skipped: skipped || 0,
},
],
})
await db.put({
...doc,
practiced: [
...doc.practiced,
{
at: new Date().valueOf(),
correct,
incorrect,
skipped: skipped || 0,
},
],
})
}

View File

@ -3,6 +3,6 @@ import { register, init } from "svelte-i18n"
register("en", () => require("./translation/en.json"))
init({
fallbackLocale: "en",
initialLocale: "en",
fallbackLocale: "en",
initialLocale: "en",
})

View File

@ -2,40 +2,42 @@ import shuffle from "lodash.shuffle"
import uniqBy from "lodash.uniqby"
export const prepareChallenge = ({
currentChallenge,
alternativeChallenges,
typeToSelect,
hasFakeOption = null,
currentChallenge,
alternativeChallenges,
typeToSelect,
hasFakeOption = null,
}) => {
const numberOfCards = hasFakeOption ? 4 : 3
const correctOption = {
...currentChallenge,
correct: true,
}
const numberOfCards = hasFakeOption ? 4 : 3
const correctOption = {
...currentChallenge,
correct: true,
}
const incorrectOptions = alternativeChallenges
.filter(({ type }) => type === typeToSelect)
.filter(
({ formInTargetLanguage }) => formInTargetLanguage !== correctOption.formInTargetLanguage
)
.map((challenge) => ({
...challenge,
correct: false,
}))
const incorrectOptions = alternativeChallenges
.filter(({ type }) => type === typeToSelect)
.filter(
({ formInTargetLanguage }) =>
formInTargetLanguage !== correctOption.formInTargetLanguage
)
.map((challenge) => ({
...challenge,
correct: false,
}))
const incorrectOptionsSample = shuffle(uniqBy(incorrectOptions, "formInTargetLanguage"))
.slice(0, numberOfCards - 1)
const incorrectOptionsSample = shuffle(
uniqBy(incorrectOptions, "formInTargetLanguage")
).slice(0, numberOfCards - 1)
const incorrectOptionsWithFake =
const incorrectOptionsWithFake =
incorrectOptions.length >= 2
? [
{
...incorrectOptionsSample[0],
fake: true,
},
...incorrectOptionsSample.slice(1),
? [
{
...incorrectOptionsSample[0],
fake: true,
},
...incorrectOptionsSample.slice(1),
]
: []
: []
return shuffle([correctOption, ...incorrectOptionsWithFake])
return shuffle([correctOption, ...incorrectOptionsWithFake])
}

View File

@ -1,37 +1,37 @@
import { Howl, Howler } from "howler"
const disableHowlerForCypress = () => {
if (
Howler.volume() !== 0 &&
if (
Howler.volume() !== 0 &&
typeof window !== "undefined" &&
window.Cypress
) {
Howler.volume(0)
console.log("Sounds while testing with cypress are disabled")
}
) {
Howler.volume(0)
console.log("Sounds while testing with cypress are disabled")
}
}
const sound = {
correct: new Howl({
src: ["sound/correct.mp3"],
onload: disableHowlerForCypress,
}),
wrong: new Howl({
src: ["sound/wrong.mp3"],
onload: disableHowlerForCypress,
}),
fanfare: new Howl({
src: ["sound/fanfare.mp3"],
onload: disableHowlerForCypress,
}),
correct: new Howl({
src: ["sound/correct.mp3"],
onload: disableHowlerForCypress,
}),
wrong: new Howl({
src: ["sound/wrong.mp3"],
onload: disableHowlerForCypress,
}),
fanfare: new Howl({
src: ["sound/fanfare.mp3"],
onload: disableHowlerForCypress,
}),
}
export const playAudio = (type, filename) => {
disableHowlerForCypress()
disableHowlerForCypress()
new Howl({
src: [`${type}/${filename}.mp3`],
}).play()
new Howl({
src: [`${type}/${filename}.mp3`],
}).play()
}
export default sound

View File

@ -51,10 +51,6 @@
}
</script>
<script lang="typescript">
export let segment
</script>
<svelte:head>
<meta name="twitter:card" content="summary_large_image" />
<meta
@ -88,4 +84,4 @@
<main style="{theme}">
<slot />
</main>
</main>

View File

@ -1,118 +1,118 @@
import { sortChallengeGroups, removeAlternatives } from "../_logic"
describe("removeAlternatives", () => {
it("doesn't change empty array", () => {
expect(removeAlternatives([])).toEqual([])
})
it("doesn't change empty array", () => {
expect(removeAlternatives([])).toEqual([])
})
it("removes duplicates", () => {
expect(removeAlternatives([{ id: 2 }, { id: 2 }])).toEqual([{ id: 2 }])
})
it("removes duplicates", () => {
expect(removeAlternatives([{ id: 2 }, { id: 2 }])).toEqual([{ id: 2 }])
})
it("returns correct number of items", () => {
expect(removeAlternatives([{ id: 2 }, { id: 3 }])).toHaveLength(2)
})
it("returns correct number of items", () => {
expect(removeAlternatives([{ id: 2 }, { id: 3 }])).toHaveLength(2)
})
})
describe("sortChallengeGroups", () => {
it("returns correct value", () => {
expect(
sortChallengeGroups(
[
{
group: "a",
priority: 0,
},
],
10
)
).toEqual([
{
group: "a",
priority: 0,
},
])
})
it("returns correct value", () => {
expect(
sortChallengeGroups(
[
{
group: "a",
priority: 0,
},
],
10
)
).toEqual([
{
group: "a",
priority: 0,
},
])
})
it("returns correct value 2", () => {
expect(
sortChallengeGroups(
[
{
id: "a1",
group: "a",
priority: 1,
},
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "c0",
group: "c",
priority: 0,
},
{
id: "a0",
group: "a",
priority: 0,
},
],
10
)
).toEqual([
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "c0",
group: "c",
priority: 0,
},
{
id: "a0",
group: "a",
priority: 0,
},
{
id: "a1",
group: "a",
priority: 1,
},
])
})
it("returns correct value 2", () => {
expect(
sortChallengeGroups(
[
{
id: "a1",
group: "a",
priority: 1,
},
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "c0",
group: "c",
priority: 0,
},
{
id: "a0",
group: "a",
priority: 0,
},
],
10
)
).toEqual([
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "c0",
group: "c",
priority: 0,
},
{
id: "a0",
group: "a",
priority: 0,
},
{
id: "a1",
group: "a",
priority: 1,
},
])
})
it("returns correct value 3", () => {
expect(
sortChallengeGroups(
[
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "a0",
group: "a",
priority: 0,
},
],
10
)
).toEqual([
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "a0",
group: "a",
priority: 0,
},
])
})
it("returns correct value 3", () => {
expect(
sortChallengeGroups(
[
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "a0",
group: "a",
priority: 0,
},
],
10
)
).toEqual([
{
id: "b1",
group: "b",
priority: 1,
},
{
id: "a0",
group: "a",
priority: 0,
},
])
})
})

View File

@ -2,66 +2,66 @@ import shuffle from "lodash.shuffle"
import uniq from "lodash.uniq"
export const removeAlternatives = (challenges) =>
Object.values(
Object.fromEntries(challenges.map((challenge) => [challenge.id, challenge]))
)
Object.values(
Object.fromEntries(challenges.map((challenge) => [challenge.id, challenge]))
)
export const sortChallengeGroups = (challenges, expectedNumberOfChallenges) => {
// This is a very inefficient sorting algorithm to make sure that random order is preserved
// as much as possible while also priorities are respected within groups
// this is useful because some challenges should precede others
// This is a very inefficient sorting algorithm to make sure that random order is preserved
// as much as possible while also priorities are respected within groups
// this is useful because some challenges should precede others
const allGroups = uniq(challenges.map(({ group }) => group))
const challengesPerGroup = Math.round(challenges.length / allGroups.length)
const expectedNumberOfGroups = Math.max(
1,
Math.round(expectedNumberOfChallenges / challengesPerGroup)
)
const acceptedGroups = shuffle(allGroups).slice(0, expectedNumberOfGroups)
const allGroups = uniq(challenges.map(({ group }) => group))
const challengesPerGroup = Math.round(challenges.length / allGroups.length)
const expectedNumberOfGroups = Math.max(
1,
Math.round(expectedNumberOfChallenges / challengesPerGroup)
)
const acceptedGroups = shuffle(allGroups).slice(0, expectedNumberOfGroups)
const allowedChallenges = challenges.filter(({ group }) =>
acceptedGroups.includes(group)
)
const challengesWithPosition = removeAlternatives(allowedChallenges).map(
(item, index) => ({
item,
index,
})
)
const allowedChallenges = challenges.filter(({ group }) =>
acceptedGroups.includes(group)
)
const challengesWithPosition = removeAlternatives(allowedChallenges).map(
(item, index) => ({
item,
index,
})
)
const isSmallestInGroup = (itemToCheck) =>
challengesWithPosition.filter(
({ item }) =>
item.group === itemToCheck.group && item.priority < itemToCheck.priority
).length === 0
const isSmallestInGroup = (itemToCheck) =>
challengesWithPosition.filter(
({ item }) =>
item.group === itemToCheck.group && item.priority < itemToCheck.priority
).length === 0
const sortedResults = []
let bestItem
let bestItemIndex
while (challengesWithPosition.length > 0) {
bestItem = challengesWithPosition[0]
bestItemIndex = 0
const sortedResults = []
let bestItem
let bestItemIndex
while (challengesWithPosition.length > 0) {
bestItem = challengesWithPosition[0]
bestItemIndex = 0
// Make sure that we prioritize and element that is the first in its group
challengesWithPosition.forEach(({ index, item }, position) => {
if (isSmallestInGroup(item)) {
bestItem = { index, item }
bestItemIndex = position
}
})
// Make sure that we prioritize and element that is the first in its group
challengesWithPosition.forEach(({ index, item }, position) => {
if (isSmallestInGroup(item)) {
bestItem = { index, item }
bestItemIndex = position
}
})
// Make sure that we prioritize based on the random sort
challengesWithPosition.forEach(({ index, item }, position) => {
if (bestItem.index > index) {
if (!isSmallestInGroup(item)) return
bestItem = { index, item }
bestItemIndex = position
}
})
// Make sure that we prioritize based on the random sort
challengesWithPosition.forEach(({ index, item }, position) => {
if (bestItem.index > index) {
if (!isSmallestInGroup(item)) return
bestItem = { index, item }
bestItemIndex = position
}
})
sortedResults.push(bestItem.item)
challengesWithPosition.splice(bestItemIndex, 1)
}
sortedResults.push(bestItem.item)
challengesWithPosition.splice(bestItemIndex, 1)
}
return sortedResults
return sortedResults
}

View File

@ -9,12 +9,12 @@ const { PORT, NODE_ENV } = process.env
const dev = NODE_ENV === "development"
polka() // You can also use Express
.use(
"/",
compression({ threshold: 0 }),
sirv("static", { dev }),
sapper.middleware()
)
.listen(PORT, (err) => {
if (err) console.log("error", err)
})
.use(
"/",
compression({ threshold: 0 }),
sirv("static", { dev }),
sapper.middleware()
)
.listen(PORT, (err) => {
if (err) console.log("error", err)
})

View File

@ -2,88 +2,87 @@ import { timestamp, files, shell } from "@sapper/service-worker"
// eslint-disable-next-line no-constant-condition
if (false) {
const ASSETS = `cache${timestamp}`
const ASSETS = `cache${timestamp}`
// `shell` is an array of all the files generated by the bundler,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(files)
const cached = new Set(to_cache)
// `shell` is an array of all the files generated by the bundler,
// `files` is an array of everything in the `static` directory
const to_cache = shell.concat(files)
const cached = new Set(to_cache)
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(ASSETS)
.then((cache) => cache.addAll(to_cache))
.then(() => {
self.skipWaiting()
})
)
})
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(ASSETS)
.then((cache) => cache.addAll(to_cache))
.then(() => {
self.skipWaiting()
})
)
})
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then(async (keys) => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key)
}
self.clients.claim()
})
)
})
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET" || event.request.headers.has("range"))
return
const url = new URL(event.request.url)
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith("http")) return
// ignore dev server requests
if (
url.hostname === self.location.hostname &&
url.port !== self.location.port
)
return
// always serve static files and bundler-generated assets from cache
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request))
return
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then(async (keys) => {
// delete old caches
for (const key of keys) {
if (key !== ASSETS) await caches.delete(key)
}
// for pages, you might want to serve a shell `service-worker-index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
self.clients.claim()
})
)
})
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET" || event.request.headers.has("range"))
return
const url = new URL(event.request.url)
// don't try to handle e.g. data: URIs
if (!url.protocol.startsWith("http")) return
// ignore dev server requests
if (
url.hostname === self.location.hostname &&
url.port !== self.location.port
)
return
// always serve static files and bundler-generated assets from cache
if (url.host === self.location.host && cached.has(url.pathname)) {
event.respondWith(caches.match(event.request))
return
}
// for pages, you might want to serve a shell `service-worker-index.html` file,
// which Sapper has generated for you. It's not right for every
// app, but if it's right for yours then uncomment this section
/*
if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) {
event.respondWith(caches.match('/service-worker-index.html'));
return;
}
*/
if (event.request.cache === "only-if-cached") return
if (event.request.cache === "only-if-cached") return
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches.open(`offline${timestamp}`).then(async (cache) => {
try {
const response = await fetch(event.request)
cache.put(event.request, response.clone())
return response
} catch (err) {
const response = await cache.match(event.request)
if (response) return response
// for everything else, try the network first, falling back to
// cache if the user is offline. (If the pages never change, you
// might prefer a cache-first approach to a network-first one.)
event.respondWith(
caches.open(`offline${timestamp}`).then(async (cache) => {
try {
const response = await fetch(event.request)
cache.put(event.request, response.clone())
return response
} catch (err) {
const response = await cache.match(event.request)
if (response) return response
throw err
}
})
)
})
throw err
}
})
)
})
}

View File

@ -5,15 +5,15 @@ const isDevelopment =
!process.env.NODE_ENV || process.env.NODE_ENV === "development"
const database = !isDevelopment
? settings.database.production
: settings.database.development
? settings.database.production
: settings.database.development
const authEnabled = Cookies.get("authEnabled") !== "false" || process.browser
export default {
...settings,
database,
features: {
authEnabled,
},
...settings,
database,
features: {
authEnabled,
},
}

View File

@ -1,6 +1,6 @@
import type { SkillType } from "./SkillType"
export type ModuleType = {
title: string,
skills: SkillType[],
title: string
skills: SkillType[]
}

View File

@ -1,9 +1,9 @@
export type SkillType = {
practiceHref: string,
title: string,
levels: number,
introduction: string,
id: string,
imageSet: string[],
summary: string,
practiceHref: string
title: string
levels: number
introduction: string
id: string
imageSet: string[]
summary: string
}

View File

@ -1,3 +1,5 @@
export default function isBrowser(): boolean {
return new Boolean((process as any).browser).valueOf()
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return new Boolean(process.browser).valueOf()
}

View File

@ -1,5 +1,5 @@
export default function isCypress(): boolean {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return window.isCypress === true
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return window.isCypress === true
}

View File

@ -1,8 +1,8 @@
import showdown from "showdown"
export default function loadMarkdownModule(module) {
const { default: text } = module
const converter = new showdown.Converter()
const html = converter.makeHtml(text)
return html
const { default: text } = module
const converter = new showdown.Converter()
const html = converter.makeHtml(text)
return html
}

View File

@ -1,162 +1,161 @@
const webpack = require("webpack")
const path = require("path")
const config = require("sapper/config/webpack.js")
const pkg = require("./package.json")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const {typescript, sass} = require("svelte-preprocess")
const CopyPlugin = require("copy-webpack-plugin")
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin
const webpack = require("webpack") // eslint-disable-line @typescript-eslint/no-var-requires
const path = require("path") // eslint-disable-line @typescript-eslint/no-var-requires
const config = require("sapper/config/webpack.js") // eslint-disable-line @typescript-eslint/no-var-requires
const pkg = require("./package.json") // eslint-disable-line @typescript-eslint/no-var-requires
const MiniCssExtractPlugin = require("mini-css-extract-plugin") // eslint-disable-line @typescript-eslint/no-var-requires
const { typescript, sass } = require("svelte-preprocess") // eslint-disable-line @typescript-eslint/no-var-requires
const CopyPlugin = require("copy-webpack-plugin") // eslint-disable-line @typescript-eslint/no-var-requires
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin // eslint-disable-line @typescript-eslint/no-var-requires
const mode = process.env.NODE_ENV
const dev = mode === "development"
const alias = { svelte: path.resolve("node_modules", "svelte") }
const extensions = [".mjs", ".js", ".ts", ".json", ".svelte", ".html"]
const mainFields = ["svelte", "module", "browser", "main"]
module.exports = {
client: {
entry: config.client.entry(),
output: config.client.output(),
resolve: { alias, extensions, mainFields },
module: {
rules: [
{
test: /\.md/i,
use: "raw-loader",
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "fonts/",
},
},
],
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
},
{
loader: "sass-loader",
options: {
sourceMap: true,
// options...
},
},
],
},
{
test: /\.(svelte|html)$/,
use: {
loader: "svelte-loader",
options: {
preprocess: [
// TO-DO: config?
typescript({}),
sass({}),
],
dev,
hydratable: true,
hotReload: false, // pending https://github.com/sveltejs/svelte/issues/2377
},
},
},
client: {
entry: config.client.entry(),
output: config.client.output(),
resolve: { alias, extensions, mainFields },
module: {
rules: [
{
test: /\.md/i,
use: "raw-loader",
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [
{
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "fonts/",
},
},
],
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: "css-loader",
},
{
loader: "sass-loader",
options: {
sourceMap: true,
// options...
},
},
],
},
{
test: /\.(svelte|html)$/,
use: {
loader: "svelte-loader",
options: {
preprocess: [
// TO-DO: config?
{
test: /\.ts$/,
loader: "ts-loader" ,
},
],
typescript({}),
sass({}),
],
dev,
hydratable: true,
hotReload: false, // pending https://github.com/sveltejs/svelte/issues/2377
},
},
},
mode,
plugins: [
...(process.env.ANALYZE === "true" ? [new BundleAnalyzerPlugin()] : []),
new CopyPlugin([
{
from: path.resolve(
"node_modules",
"@openfonts/noto-sans_all",
"files"
),
to: path.resolve("static", "files"),
},
]),
new MiniCssExtractPlugin({
filename: "css/mystyles.css",
}),
// pending https://github.com/sveltejs/svelte/issues/2377
// dev && new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
"process.browser": true,
"process.env.NODE_ENV": JSON.stringify(mode),
}),
].filter(Boolean),
devtool: dev && "inline-source-map",
performance: {
hints: "error",
maxEntrypointSize: 370000,
maxAssetSize: 256000
// TO-DO: config?
{
test: /\.ts$/,
loader: "ts-loader",
},
],
},
mode,
plugins: [
...(process.env.ANALYZE === "true" ? [new BundleAnalyzerPlugin()] : []),
new CopyPlugin([
{
from: path.resolve(
"node_modules",
"@openfonts/noto-sans_all",
"files"
),
to: path.resolve("static", "files"),
},
]),
new MiniCssExtractPlugin({
filename: "css/mystyles.css",
}),
// pending https://github.com/sveltejs/svelte/issues/2377
// dev && new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
"process.browser": true,
"process.env.NODE_ENV": JSON.stringify(mode),
}),
].filter(Boolean),
devtool: dev && "inline-source-map",
performance: {
hints: "error",
maxEntrypointSize: 370000,
maxAssetSize: 256000,
},
},
server: {
entry: config.server.entry(),
output: config.server.output(),
target: "node",
resolve: { alias, extensions, mainFields },
externals: Object.keys(pkg.dependencies).concat("encoding"),
module: {
rules: [
{
test: /\.md/i,
use: "raw-loader",
},
{
test: /\.(svelte|html)$/,
use: {
loader: "svelte-loader",
options: {
preprocess: [
// TO-DO: config?
typescript({}),
sass({}),
],
css: false,
generate: "ssr",
dev,
},
},
},
server: {
entry: config.server.entry(),
output: config.server.output(),
target: "node",
resolve: { alias, extensions, mainFields },
externals: Object.keys(pkg.dependencies).concat("encoding"),
module: {
rules: [
{
test: /\.md/i,
use: "raw-loader",
},
{
test: /\.(svelte|html)$/,
use: {
loader: "svelte-loader",
options: {
preprocess: [
// TO-DO: config?
{
test: /\.ts$/,
loader: "ts-loader" ,
},
],
typescript({}),
sass({}),
],
css: false,
generate: "ssr",
dev,
},
},
},
mode: process.env.NODE_ENV,
performance: {
hints: false,
// TO-DO: config?
{
test: /\.ts$/,
loader: "ts-loader",
},
],
},
mode: process.env.NODE_ENV,
performance: {
hints: false,
},
},
serviceworker: {
entry: config.serviceworker.entry(),
output: config.serviceworker.output(),
mode: process.env.NODE_ENV,
},
serviceworker: {
entry: config.serviceworker.entry(),
output: config.serviceworker.output(),
mode: process.env.NODE_ENV,
},
}

View File

@ -1,6 +1,6 @@
module.exports = {
presets: [
["@babel/preset-env", {targets: {node: "current"}}],
"@babel/preset-typescript",
],
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
}

View File

@ -4,197 +4,197 @@
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
"moduleNameMapper": {
"\\.(md)$": "<rootDir>/apps/web/assetsTransformer.js",
},
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
moduleNameMapper: {
"\\.(md)$": "<rootDir>/apps/web/assetsTransformer.js",
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
// "transform": {
// "^.+\\.md?$": "markdown-loader-jest"
// },
// A map from regular expressions to paths to transformers
// transform: undefined,
// "transform": {
// "^.+\\.md?$": "markdown-loader-jest"
// },
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
// Whether to use watchman for file crawling
// watchman: true,
}

View File

@ -11,8 +11,8 @@
"test:ci": "yarn web test:ci",
"web": "yarn workspace @librelingo/web",
"types": "yarn workspaces run types",
"stylefix": "yarn web run prettierfix && yarn web run eslintfix",
"stylecheck": "yarn web run prettiercheck && yarn web run eslintcheck",
"format": "eslint . --fix --max-warnings=0",
"lint": "eslint . --max-warnings=0",
"exportAllCourses": "./scripts/exportAllYamlCourses.sh",
"exportCourse": "./scripts/exportYamlCourse.sh",
"docs": "poetry run mkdocs serve",
@ -44,9 +44,12 @@
"@typescript-eslint/eslint-plugin": "5.9.1",
"@typescript-eslint/parser": "5.9.1",
"eslint": "8.7.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-cypress": "2.12.1",
"eslint-plugin-jest": "25.7.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-svelte3": "3.4.0",
"prettier": "2.5.1",
"prettier-plugin-svelte": "^2.6.0",
"sapper": "0.29.3",
"semantic-release": "18.0.1",

View File

@ -7151,6 +7151,11 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@^8.5.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
eslint-plugin-cypress@2.12.1:
version "2.12.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz#9aeee700708ca8c058e00cdafe215199918c2632"
@ -7165,6 +7170,13 @@ eslint-plugin-jest@25.7.0:
dependencies:
"@typescript-eslint/experimental-utils" "^5.0.0"
eslint-plugin-prettier@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz#8b99d1e4b8b24a762472b4567992023619cb98e0"
integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-svelte3@3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-svelte3/-/eslint-plugin-svelte3-3.4.0.tgz#0fe6cfcd42a53ff346082d47e7386be66bd8d90e"
@ -7530,6 +7542,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^3.1.1:
version "3.2.4"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
@ -12345,7 +12362,14 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
prettier-plugin-svelte@2.6.0, prettier-plugin-svelte@^2.6.0:
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier-plugin-svelte@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.6.0.tgz#0e845b560b55cd1d951d6c50431b4949f8591746"
integrity sha512-NPSRf6Y5rufRlBleok/pqg4+1FyGsL0zYhkYP6hnueeL1J/uCm3OfOZPsLX4zqD9VAdcXfyEL2PYqGv8ZoOSfA==