diff --git a/README.md b/README.md index 23062b26..bd33323c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ Vous pouvez également lancer les tests en local avec : npm run test ``` +Les tests sont dans le dossier `tests/` et finissent en `.test.ts`. Les autres +fichiers `.ts` servent à configurer l'environnement de test. En particulier, le +vrai store Baredux est remplacé par un faux store avec des fausses données comme +si on était connecté à Scribouilli et qu'on avait ouvert un site. Ces fausses +données sont dans `tests/fake-store.ts`. + ### Note Pour rendre accessible une nouvelle route : diff --git a/assets/scripts/actions/current-repository.ts b/assets/scripts/actions/current-repository.ts index 1075b45e..25f33149 100644 --- a/assets/scripts/actions/current-repository.ts +++ b/assets/scripts/actions/current-repository.ts @@ -1,7 +1,7 @@ import page from 'page' import yaml from 'js-yaml' -import store, { type PartialStore } from './../store.ts' +import store from './../store.ts' import ScribouilliGitRepo, { makeRepoId, makePublicRepositoryURL, @@ -248,13 +248,8 @@ const getCurrentRepoConfig = (): Promise => { .catch(handleErrors) } -const defaultStore = store export function saveCustomCSS( css: string, - store: PartialStore< - 'currentRepository' | 'gitAgent', - 'setTheme' - > = defaultStore, ): ReturnType | Promise { if (!store.state.currentRepository || !store.state.gitAgent) { return Promise.resolve() diff --git a/assets/scripts/actions/file.ts b/assets/scripts/actions/file.ts index 6aefe96d..f25296bd 100644 --- a/assets/scripts/actions/file.ts +++ b/assets/scripts/actions/file.ts @@ -1,16 +1,15 @@ import GitAgent from '../GitAgent.ts' -import store, { type PartialStore } from '../store.ts' +import store from '../store.ts' export const writeFileAndCommit = async ( fileName: string, content: string | Uint8Array, commitMessage?: string, - localStore: PartialStore<'gitAgent', never> = store, ): Promise => { if (typeof commitMessage !== 'string' || commitMessage === '') { commitMessage = `Modification du fichier ${fileName}` } - const { gitAgent } = localStore.state + const { gitAgent } = store.state if (!gitAgent) { throw new TypeError('gitAgent is undefined') @@ -24,9 +23,8 @@ export const writeFileAndPushChanges = async ( fileName: string, content: string | Uint8Array, commitMessage: string = '', - localStore = store, ): ReturnType => { - const { gitAgent } = localStore.state + const { gitAgent } = store.state if (!gitAgent) { throw new TypeError('gitAgent is undefined') @@ -39,9 +37,8 @@ export const writeFileAndPushChanges = async ( export const deleteFileAndCommit = async ( fileName: string, commitMessage: string = '', - localStore = store, ): ReturnType => { - const { gitAgent } = localStore.state + const { gitAgent } = store.state if (!gitAgent) { throw new TypeError('gitAgent is undefined') @@ -58,15 +55,14 @@ export const deleteFileAndCommit = async ( export const deleteFileAndPushChanges = ( fileName: string, commitMessage: string, - localStore = store, ): ReturnType => { - const { gitAgent } = localStore.state + const { gitAgent } = store.state if (!gitAgent) { throw new TypeError('gitAgent is undefined') } - deleteFileAndCommit(fileName, commitMessage, localStore) + deleteFileAndCommit(fileName, commitMessage) return gitAgent.safePush() } diff --git a/assets/scripts/store.ts b/assets/scripts/store.ts index 90875ab5..2ac1e55e 100644 --- a/assets/scripts/store.ts +++ b/assets/scripts/store.ts @@ -3,6 +3,7 @@ import GitAgent from './GitAgent.ts' import ScribouilliGitRepo from './scribouilliGitRepo.ts' import type { Page, Article } from './types/atelier.ts' import type { BuildStatus } from './types/git.ts' + /** * Un store baredux a pour vocation de refléter notamment le modèle mental de la * personne face à Scribouilli. Le store stocke donc principalement des données (et parfois des singletons) @@ -62,7 +63,7 @@ const state: ScribouilliState = { }, } -const mutations = { +export const mutations = { setOAuthProvider( state: ScribouilliState, oAuthProvider: ScribouilliState['oAuthProvider'], @@ -177,16 +178,4 @@ const store: BareduxStore = Store({ mutations, }) -type PartialMutations = { - [mutation in Mutations]: (state: State, ...others: any[]) => void | State -} - -export type PartialStore< - S extends keyof ScribouilliState = keyof ScribouilliState, - M extends keyof ScribouilliMutations = keyof ScribouilliMutations, -> = BareduxStore< - Pick, - PartialMutations, M> -> - export default store diff --git a/assets/scripts/utils.ts b/assets/scripts/utils.ts index 0000e07d..55c713fc 100644 --- a/assets/scripts/utils.ts +++ b/assets/scripts/utils.ts @@ -92,7 +92,9 @@ export const logMessage = ( export const delay = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)) -export const isItStillCompiling = (lastCommit: CommitObject): boolean => { +export const isItStillCompiling = ( + lastCommit: Pick, +): boolean => { // Delay (in seconds) after which a non-updated website is assumed to have failed to build. const ERROR_DELAY = 60 * 1000 const currentTime = new Date().getTime() / 1000 diff --git a/package.json b/package.json index 36e3371f..aa66c879 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "ts": "tsc && echo 'Tout va bien 🎉'", "dev": "rollup -c -w", "build": "rollup -c", - "test": "mocha --recursive tests --extension ts", + "test": "mocha --recursive tests --extension ts --node-option import=./tests/hook.ts", "prepare": "husky install", "svelte-check": "svelte-check --tsconfig tsconfig.json" }, diff --git a/tests/actions/current-repository.test.ts b/tests/actions/current-repository.test.ts index 4f433773..e6d40d78 100644 --- a/tests/actions/current-repository.test.ts +++ b/tests/actions/current-repository.test.ts @@ -5,36 +5,18 @@ import { CUSTOM_CSS_PATH } from '../../assets/scripts/config.ts' import { describe } from 'mocha' import sinon from 'sinon' import { expect } from 'chai' -import GitAgent from '../../assets/scripts/GitAgent.ts' describe('actions/current-repository.ts', () => { + beforeEach(() => { + sinon.reset() + }) + describe('saveCustomCSS', () => { it('calls writeFile, commit and safePush', done => { - const fakeStore = { - mutations: { - setTheme: sinon.stub(), - }, - state: { - currentRepository: { - origin: 'github.com', - publicRepositoryURL: 'https://github.com/test/site', - owner: 'test', - repoName: 'site', - repoId: 'test-site', - publishedWebsiteURL: Promise.resolve('https://test.github.io/site'), - }, - gitAgent: new GitAgent({ - auth: {}, - remoteURL: 'https://github.com', - repoId: 'test-site', - }), - }, - subscribe: sinon.stub(), - } sinon.stub(file, 'writeFileAndPushChanges') const css = 'body { background: pink; }' - saveCustomCSS(css, fakeStore) + saveCustomCSS(css) expect(file.writeFileAndPushChanges).to.have.been.calledWithExactly( CUSTOM_CSS_PATH, diff --git a/tests/actions/file.test.ts b/tests/actions/file.test.ts index 13b927d4..6a332485 100644 --- a/tests/actions/file.test.ts +++ b/tests/actions/file.test.ts @@ -1,52 +1,29 @@ import './../setup.ts' -import { fakeStateWithOneSite } from './../fixtures.ts' import { writeFileAndCommit, writeFileAndPushChanges, deleteFileAndCommit, deleteFileAndPushChanges, } from './../../assets/scripts/actions/file.ts' -import gitAgent from './../../assets/scripts/GitAgent.ts' import store from './../../assets/scripts/store.ts' import sinon from 'sinon' import { expect } from 'chai' -// Use a common sandbox for all tests so we can easily restore it after each test. -const sandbox = sinon.createSandbox() - describe('actions/file.ts', () => { beforeEach(() => { - sandbox.stub(store, 'state').value(fakeStateWithOneSite) - }) - - afterEach(() => { - sandbox.restore() + sinon.reset() }) describe('writeFileAndCommit', () => { it('calls writeFile and commit', done => { - const fakeStore = { - state: { - gitAgent: { - writeFile: sinon.stub(), - commit: sinon.stub(), - }, - }, - mutations: {}, - } - writeFileAndCommit( - 'test.ts', - 'Curiouser and curiouser!', - undefined, - fakeStore, - ) + writeFileAndCommit('test.ts', 'Curiouser and curiouser!', undefined) done() - expect(fakeStore.state.gitAgent.writeFile).to.have.been.calledWith( + expect(store.state.gitAgent!.writeFile).to.have.been.calledWith( 'bla', 'test.ts', 'Curiouser and curiouser!', ) - expect(fakeStore.state.gitAgent.commit).to.have.been.calledWith( + expect(store.state.gitAgent!.commit).to.have.been.calledWith( 'fanne', 'Modification du fichier test.ts', ) @@ -55,31 +32,18 @@ describe('actions/file.ts', () => { describe('writeFileAndPushChanges', () => { it('calls writeFile, commit and push', done => { - const fakeStore = { - state: { - gitAgent: { - removeFile: sinon.stub(), - commit: sinon.stub(), - }, - }, - } - writeFileAndPushChanges( - 'test.ts', - 'Curiouser and curiouser!', - undefined, - fakeStore, - ) + writeFileAndPushChanges('test.ts', 'Curiouser and curiouser!') done() - expect(gitAgent.writeFile).to.have.been.calledWith( + expect(store.state.gitAgent!.writeFile).to.have.been.calledWith( store.state.currentRepository, 'test.ts', 'Curiouser and curiouser!', ) - expect(gitAgent.commit).to.have.been.calledWith( + expect(store.state.gitAgent!.commit).to.have.been.calledWith( store.state.currentRepository, `Modification du fichier test.js`, ) - expect(gitAgent.safePush).to.have.been.calledWith( + expect(store.state.gitAgent!.safePush).to.have.been.calledWith( store.state.currentRepository, ) }) @@ -87,35 +51,17 @@ describe('actions/file.ts', () => { describe('deleteFileAndCommit', () => { it('calls removeFile and commit', done => { - const fakeStore = { - state: { - gitAgent: { - removeFile: sinon.stub(), - commit: sinon.stub(), - }, - }, - } - - deleteFileAndCommit('test.ts', undefined, fakeStore) + deleteFileAndCommit('test.ts') done() - expect(fakeStore.state.gitAgent.removeFile).to.have.been.calledOnce - expect(fakeStore.state.gitAgent.commit).to.have.been.calledOnce + expect(store.state.gitAgent!.removeFile).to.have.been.calledOnce + expect(store.state.gitAgent!.commit).to.have.been.calledOnce }) }) describe('deleteFileAndPushChanges', () => { it('calls removeFile and commit', done => { - const fakeStore = { - state: { - gitAgent: { - removeFile: sinon.stub(), - safePush: sinon.stub(), - commit: sinon.stub(), - }, - }, - } - deleteFileAndPushChanges('test.ts', undefined, fakeStore) - expect(fakeStore.state.gitAgent.safePush).to.have.been.calledOnce + deleteFileAndPushChanges('test.ts', 'Suppression de test.ts') + expect(store.state.gitAgent!.safePush).to.have.been.calledOnce done() }) }) diff --git a/tests/fake-store.ts b/tests/fake-store.ts new file mode 100644 index 00000000..255646fa --- /dev/null +++ b/tests/fake-store.ts @@ -0,0 +1,93 @@ +// c'est le store qui est chargé dans les tests via hooks.ts + +import Store, { type BareduxStore } from 'baredux' +import type { ScribouilliState } from '../assets/scripts/store' +import * as sinon from 'sinon' +import GitAgent from '../assets/scripts/GitAgent.ts' +// @ts-ignore Le ?no-fake permet de charger le vrai store (dont on veut +// réutiliser les mutations) sans que le resolver dans `hook.ts` tente de +// charger ce fichier (`fake-store.ts`) à nouveau. En pratique, ce bout est +// ignoré par le resolver fournit par Node.js, et donc ça charge vraiment le +// fichier `store.ts`. +import { mutations } from '../assets/scripts/store.ts?no-fake' + +const gitAgentFunctions: (keyof GitAgent)[] = [ + 'branch', + 'checkFileExistence', + 'checkout', + 'clone', + 'commit', + 'currentBranch', + 'currentCommit', + 'falliblePush', + 'fetch', + 'fetchAndTryMerging', + 'forcePush', + 'getFile', + 'listBranches', + 'listFiles', + 'listRemotes', + 'pullOrCloneRepo', + 'removeFile', + 'safePush', + 'setAuthor', + 'tryMerging', + 'writeFile', +] + +for (const func of gitAgentFunctions) { + sinon.stub(GitAgent.prototype, func) +} + +const MON_PETIT_SITE = { + origin: 'https://github.com', + owner: 'utilisateurice', + publicRepositoryURL: 'https://github.com/utilisateurice/mon-petit-site', + repoName: 'mon-petit-site', + publishedWebsiteURL: Promise.resolve( + 'https://utilisateurice.github.io/mon-petit-site', + ), + repoId: 'utilisateurice-mon-petit-site', +} + +const state: ScribouilliState = { + oAuthProvider: { + name: 'GitHub', + accessToken: 'top-secret', + origin: 'https://github.com', + }, + login: 'utilisateurice', + email: 'utilisateurice@courriel.fr', + currentRepository: MON_PETIT_SITE, + gitAgent: new GitAgent({ + auth: {}, + remoteURL: 'https://github.com', + repoId: MON_PETIT_SITE.repoId, + }), + conflict: undefined, + reposByAccount: { + utilisateurice: Promise.resolve([MON_PETIT_SITE]), + }, + pages: [ + { + index: 1, + path: 'index.md', + title: 'Accueil', + }, + ], + articles: undefined, + buildStatus: 'in_progress', + basePath: '', + theme: { + css: undefined, + }, +} + +type ScribouilliMutations = typeof mutations + +const store: BareduxStore = Store({ + state, + mutations, +}) + +export default store diff --git a/tests/fixtures.ts b/tests/fixtures.ts deleted file mode 100644 index cb69c5c8..00000000 --- a/tests/fixtures.ts +++ /dev/null @@ -1,38 +0,0 @@ -import GitAgent from '../assets/scripts/GitAgent.ts' - -//@ts-ignore -export const fakeStateWithOneSite = sandbox => ({ - oAuthProvider: { - origin: 'https://github.com', - accessToken: '1234567890', - name: 'github', - }, - login: 'alice', - email: 'alice@wonderland.io', - origin: undefined, - //@ts-ignore - currentRepository: { - repoName: 'alice.github.io', - owner: 'alice', - publicRepositoryURL: 'https://github.com/alice/alice.github.io.git', - origin: 'https://github.com', - }, - gitAgent: sandbox.createStubInstance(GitAgent, { - writeFile: sinon.stub().resolves(), - commit: sinon.stub().resolves(), - }), - // We use the term "account" to refer to user or organization. - reposByAccount: { - alice: [ - { - name: 'alice.github.io', - owner: 'alice', - publishedWebsiteURL: 'https://alice.github.io', - repositoryURL: 'https://github.org/alice/alice.github.io', - }, - ], - }, - buildStatus: { - setBuildingAndCheckStatusLater() {}, - }, -}) diff --git a/tests/hook.ts b/tests/hook.ts new file mode 100644 index 00000000..3e792c1a --- /dev/null +++ b/tests/hook.ts @@ -0,0 +1,17 @@ +import { registerHooks } from 'node:module' + +registerHooks({ + // Un hook qui remplace store.ts avec fake-store.ts + resolve(specifier, context, nextResolve) { + if (specifier.endsWith('/store.ts') || specifier.endsWith('/store')) { + const url = new URL(specifier, context.parentURL) + if (url.pathname.endsWith('/assets/scripts/store.ts')) { + return { + url: new URL('./fake-store.ts', import.meta.url).toString(), + shortCircuit: true, + } + } + } + return nextResolve(specifier, context) + }, +}) diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 1459b1e5..3fe701b5 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,8 +1,11 @@ import './setup.ts' import { isItStillCompiling } from '../assets/scripts/utils.ts' +import sinon from 'sinon' +import { expect } from 'chai' +import type { CommitObject } from 'isomorphic-git' describe('Utils function', () => { - let now, clock + let now: Date, clock: sinon.SinonFakeTimers beforeEach(() => { now = new Date('2024-12-03 10:00') @@ -15,35 +18,42 @@ describe('Utils function', () => { describe('#isItStillCompiling', () => { it('returns true when just commited', () => { - const lastCommit = { - committer: { - timestamp: now.getTime() / 1000, - }, - } + const lastCommit = commitObject({ timestamp: now.getTime() / 1000 }) expect(isItStillCompiling(lastCommit)).to.be.true }) it('returns true when commited since less than DELAY', () => { const commitDatetime = new Date('2024-12-03 10:29') - const lastCommit = { - committer: { - timestamp: commitDatetime.getTime() / 1000, - }, - } + const lastCommit = commitObject({ + timestamp: commitDatetime.getTime() / 1000, + }) expect(isItStillCompiling(lastCommit)).to.be.true }) it('returns false when commited since more than DELAY', () => { const commitDatetime = new Date('2024-12-02 15:30') - const lastCommit = { - committer: { - timestamp: commitDatetime.getTime() / 1000, - }, - } + const lastCommit = commitObject({ + timestamp: commitDatetime.getTime() / 1000, + }) expect(isItStillCompiling(lastCommit)).to.be.false }) }) }) + +function commitObject( + committer: Partial, +): Pick { + return { + committer: { + timestamp: 0, + name: 'Alice', + email: 'alice@example.org', + timezoneOffset: 0, + + ...committer, + }, + } +} diff --git a/tsconfig.json b/tsconfig.json index ea9f9211..288fc346 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { - "include": ["declarations.d.ts", "assets/scripts/**/*.ts"], - "exclude": ["node_modules", "tests/**/*.js"], + "include": ["declarations.d.ts", "assets/scripts/**/*.ts", "tests/**/*.ts"], + "exclude": ["node_modules"], "compilerOptions": { "allowJs": true, "checkJs": true,