chore: run prettier through eslint (#2070)
This commit is contained in:
parent
eba232b43a
commit
4ea85181da
99
.eslintrc.js
99
.eslintrc.js
|
@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
*.svelte
|
|
@ -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
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"svelteSortOrder" : "scripts-markup-styles",
|
||||
"svelteStrictMode": true,
|
||||
"allowShorthand": false,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"semi": false
|
||||
}
|
|
@ -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!",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<div class="box">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
|
||||
{#if asHref != null}
|
||||
<a class="hidden-link" href={asHref} />
|
||||
<a class="hidden-link" href={asHref}> </a>
|
||||
{/if}
|
||||
|
||||
<style type="text/scss">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<nav
|
||||
role="navigation"
|
||||
data-test-id="navbar"
|
||||
aria-label="main navigation"
|
||||
>
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"eslintfix": "exit 0",
|
||||
"prettierfix": "exit 0",
|
||||
"build": "exit 0",
|
||||
"types": "exit 0"
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
process(src, filename, config, options) {
|
||||
return
|
||||
},
|
||||
process() {
|
||||
return
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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 })
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { When } from "cypress-cucumber-preprocessor/steps"
|
||||
|
||||
When("I wait a moment", () => {
|
||||
cy.wait(1000)
|
||||
cy.wait(1000)
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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, ""))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -21,5 +21,5 @@ import "@percy/cypress"
|
|||
// require('./commands')
|
||||
|
||||
Cypress.on("window:before:load", (win) => {
|
||||
win.isCypress = true
|
||||
win.isCypress = true
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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([])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -231,7 +231,6 @@
|
|||
<div class="container">
|
||||
<FanfareScreen
|
||||
courseURL="{courseURL}"
|
||||
rawChallenges="{rawChallenges}"
|
||||
skillId="{skillId}"
|
||||
stats="{stats}"
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)}`
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
})
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { SkillType } from "./SkillType"
|
||||
|
||||
export type ModuleType = {
|
||||
title: string,
|
||||
skills: SkillType[],
|
||||
title: string
|
||||
skills: SkillType[]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
}
|
||||
|
|
280
jest.config.js
280
jest.config.js
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue