fix: running tests and adding tests to CI & CD (#1047)

* fix: running tests and adding tests to CI & CD

* fix: yarn.lock

* fix: alert service

* fix: ts-jest utils import
This commit is contained in:
Karol Sójko
2022-05-24 11:06:17 +02:00
committed by GitHub
parent c6839f776a
commit 2d3221c944
14 changed files with 158 additions and 160 deletions

View File

@@ -6,23 +6,23 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
tsc: test:
name: Check types & lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: yarn install --pure-lockfile run: yarn install --pure-lockfile
- name: Typescript - name: Bundle
run: yarn tsc run: yarn bundle
- name: ESLint - name: ESLint
run: yarn lint --quiet run: yarn lint
- name: Test
run: yarn test
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: tsc needs: test
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -10,31 +10,26 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
test:
tsc:
name: Check types & lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: yarn install --pure-lockfile run: yarn install --pure-lockfile
- name: Bundle
- name: Typescript run: yarn bundle
run: yarn tsc
- name: ESLint - name: ESLint
run: yarn lint --quiet run: yarn lint
- name: Test
run: yarn test
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: tsc needs: test
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -7,21 +7,16 @@ on:
- main - main
jobs: jobs:
test:
tsc:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: yarn install --pure-lockfile run: yarn install --pure-lockfile
- name: Bundle
- name: Typescript run: yarn bundle
run: yarn tsc
- name: ESLint - name: ESLint
run: yarn lint --quiet run: yarn lint
- name: Test
run: yarn test

View File

@@ -9,32 +9,25 @@ on:
branches: [ main ] branches: [ main ]
jobs: jobs:
test:
tsc:
name: Check types & lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: yarn install --pure-lockfile run: yarn install --pure-lockfile
- name: Bundle
- name: Typescript run: yarn bundle
run: yarn tsc
- name: ESLint - name: ESLint
run: yarn lint --quiet run: yarn lint
- name: Test
run: yarn test
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: tsc needs: test
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -2,173 +2,175 @@
* @jest-environment jsdom * @jest-environment jsdom
*/ */
import { NoteView } from './NoteView' import { WebApplication } from '@/UIModels/Application'
import { AppState } from '@/UIModels/AppState'
import { NotesState } from '@/UIModels/AppState/NotesState'
import { import {
ApplicationEvent, ApplicationEvent,
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
} from '@standardnotes/snjs/' NoteViewController,
SNNote,
} from '@standardnotes/snjs'
describe('editor-view', () => { import { NoteView } from './NoteView'
let ctrl: NoteView
let setShowProtectedWarningSpy: jest.SpyInstance
beforeEach(() => { describe('NoteView', () => {
ctrl = new NoteView({} as any) let noteViewController: NoteViewController
let application: WebApplication
let appState: AppState
let notesState: NotesState
setShowProtectedWarningSpy = jest.spyOn(ctrl, 'setShowProtectedOverlay') const createNoteView = () =>
new NoteView({
Object.defineProperties(ctrl, { controller: noteViewController,
application: { application,
value: {
getAppState: () => {
return {
notes: {
setShowProtectedWarning: jest.fn(),
},
}
},
hasProtectionSources: () => true,
authorizeNoteAccess: jest.fn(),
},
},
removeComponentsObserver: {
value: jest.fn(),
writable: true,
},
removeTrashKeyObserver: {
value: jest.fn(),
writable: true,
},
unregisterComponent: {
value: jest.fn(),
writable: true,
},
editor: {
value: {
clearNoteChangeListener: jest.fn(),
},
},
}) })
})
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers() jest.useFakeTimers()
noteViewController = {} as jest.Mocked<NoteViewController>
notesState = {} as jest.Mocked<NotesState>
notesState.setShowProtectedWarning = jest.fn()
appState = {
notes: notesState,
} as jest.Mocked<AppState>
application = {} as jest.Mocked<WebApplication>
application.getAppState = jest.fn().mockReturnValue(appState)
application.hasProtectionSources = jest.fn().mockReturnValue(true)
application.authorizeNoteAccess = jest.fn()
}) })
afterEach(() => { afterEach(() => {
jest.useRealTimers() jest.useRealTimers()
}) })
afterEach(() => {
ctrl.deinit()
})
describe('note is protected', () => { describe('note is protected', () => {
beforeEach(() => {
Object.defineProperty(ctrl, 'note', {
value: {
protected: true,
},
})
})
it("should hide the note if at the time of the session expiration the note wasn't edited for longer than the allowed idle time", async () => { it("should hide the note if at the time of the session expiration the note wasn't edited for longer than the allowed idle time", async () => {
jest const secondsElapsedSinceLastEdit = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction + 5
.spyOn(ctrl, 'getSecondsElapsedSinceLastEdit')
.mockImplementation(() => ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction + 5)
await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired) noteViewController.note = {
protected: true,
userModifiedDate: new Date(Date.now() - secondsElapsedSinceLastEdit * 1000),
} as jest.Mocked<SNNote>
expect(setShowProtectedWarningSpy).toHaveBeenCalledWith(true) await createNoteView().onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(true)
}) })
it('should postpone the note hiding by correct time if the time passed after its last modification is less than the allowed idle time', async () => { it('should postpone the note hiding by correct time if the time passed after its last modification is less than the allowed idle time', async () => {
const secondsElapsedSinceLastEdit = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - 3 const secondsElapsedSinceLastEdit = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - 3
Object.defineProperty(ctrl.note, 'userModifiedDate', { noteViewController.note = {
value: new Date(Date.now() - secondsElapsedSinceLastEdit * 1000), protected: true,
configurable: true, userModifiedDate: new Date(Date.now() - secondsElapsedSinceLastEdit * 1000),
}) } as jest.Mocked<SNNote>
await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired) await createNoteView().onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
const secondsAfterWhichTheNoteShouldHide = const secondsAfterWhichTheNoteShouldHide =
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - secondsElapsedSinceLastEdit ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - secondsElapsedSinceLastEdit
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000) jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
expect(setShowProtectedWarningSpy).not.toHaveBeenCalled()
expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
jest.advanceTimersByTime(1 * 1000) jest.advanceTimersByTime(1 * 1000)
expect(setShowProtectedWarningSpy).toHaveBeenCalledWith(true)
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(true)
}) })
it('should postpone the note hiding by correct time if the user continued editing it even after the protection session has expired', async () => { it('should postpone the note hiding by correct time if the user continued editing it even after the protection session has expired', async () => {
const secondsElapsedSinceLastModification = 3 const secondsElapsedSinceLastModification = 3
Object.defineProperty(ctrl.note, 'userModifiedDate', {
value: new Date(Date.now() - secondsElapsedSinceLastModification * 1000),
configurable: true,
})
await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired) noteViewController.note = {
protected: true,
userModifiedDate: new Date(Date.now() - secondsElapsedSinceLastModification * 1000),
} as jest.Mocked<SNNote>
await createNoteView().onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
let secondsAfterWhichTheNoteShouldHide = let secondsAfterWhichTheNoteShouldHide =
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - secondsElapsedSinceLastModification ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction - secondsElapsedSinceLastModification
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000) jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
// A new modification has just happened noteViewController.note = {
Object.defineProperty(ctrl.note, 'userModifiedDate', { protected: true,
value: new Date(), userModifiedDate: new Date(),
configurable: true, } as jest.Mocked<SNNote>
})
secondsAfterWhichTheNoteShouldHide = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction secondsAfterWhichTheNoteShouldHide = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000) jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
expect(setShowProtectedWarningSpy).not.toHaveBeenCalled() expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
jest.advanceTimersByTime(1 * 1000) jest.advanceTimersByTime(1 * 1000)
expect(setShowProtectedWarningSpy).toHaveBeenCalledWith(true) expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(true)
}) })
}) })
describe('note is unprotected', () => { describe('note is unprotected', () => {
it('should not call any hiding logic', async () => { it('should not call any hiding logic', async () => {
Object.defineProperty(ctrl, 'note', { noteViewController.note = {
value: { protected: false,
protected: false, } as jest.Mocked<SNNote>
},
})
const hideProtectedNoteIfInactiveSpy = jest.spyOn(ctrl, 'hideProtectedNoteIfInactive')
await ctrl.onAppEvent(ApplicationEvent.UnprotectedSessionExpired) await createNoteView().onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
expect(hideProtectedNoteIfInactiveSpy).not.toHaveBeenCalled() expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
}) })
}) })
describe('dismissProtectedWarning', () => { describe('dismissProtectedWarning', () => {
beforeEach(() => {
noteViewController.note = {
protected: false,
} as jest.Mocked<SNNote>
})
describe('the note has protection sources', () => { describe('the note has protection sources', () => {
it('should reveal note contents if the authorization has been passed', async () => { it('should reveal note contents if the authorization has been passed', async () => {
jest.spyOn(ctrl['application'], 'authorizeNoteAccess').mockImplementation(async () => Promise.resolve(true)) application.authorizeNoteAccess = jest.fn().mockReturnValue(true)
await ctrl.dismissProtectedWarning() const noteView = new NoteView({
controller: noteViewController,
application,
})
expect(setShowProtectedWarningSpy).toHaveBeenCalledWith(false) await noteView.dismissProtectedWarning()
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(false)
}) })
it('should not reveal note contents if the authorization has not been passed', async () => { it('should not reveal note contents if the authorization has not been passed', async () => {
jest.spyOn(ctrl['application'], 'authorizeNoteAccess').mockImplementation(async () => Promise.resolve(false)) application.authorizeNoteAccess = jest.fn().mockReturnValue(false)
await ctrl.dismissProtectedWarning() const noteView = new NoteView({
controller: noteViewController,
application,
})
expect(setShowProtectedWarningSpy).not.toHaveBeenCalled() await noteView.dismissProtectedWarning()
expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
}) })
}) })
describe('the note does not have protection sources', () => { describe('the note does not have protection sources', () => {
it('should reveal note contents', async () => { it('should reveal note contents', async () => {
jest.spyOn(ctrl['application'], 'hasProtectionSources').mockImplementation(() => false) application.hasProtectionSources = jest.fn().mockReturnValue(false)
await ctrl.dismissProtectedWarning() const noteView = new NoteView({
controller: noteViewController,
application,
})
expect(setShowProtectedWarningSpy).toHaveBeenCalledWith(false) await noteView.dismissProtectedWarning()
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(false)
}) })
}) })
}) })

View File

@@ -1,4 +1,3 @@
import { WebApplication } from '@/UIModels/Application'
import { createRef, JSX, RefObject } from 'preact' import { createRef, JSX, RefObject } from 'preact'
import { import {
ApplicationEvent, ApplicationEvent,
@@ -15,8 +14,8 @@ import {
PayloadEmitSource, PayloadEmitSource,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { debounce, isDesktopApplication } from '@/Utils' import { debounce, isDesktopApplication } from '@/Utils'
import { EventSource } from '../../UIModels/AppState/EventSource'
import { KeyboardModifier, KeyboardKey } from '@/Services/IOService' import { KeyboardModifier, KeyboardKey } from '@/Services/IOService'
import { EventSource } from '@/UIModels/AppState'
import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Strings' import { STRING_DELETE_PLACEHOLDER_ATTEMPT, STRING_DELETE_LOCKED_ATTEMPT, StringDeleteNote } from '@/Strings'
import { confirmDialog } from '@/Services/AlertService' import { confirmDialog } from '@/Services/AlertService'
import { PureComponent } from '@/Components/Abstract/PureComponent' import { PureComponent } from '@/Components/Abstract/PureComponent'
@@ -35,6 +34,7 @@ import {
transactionForDisassociateComponentWithCurrentNote, transactionForDisassociateComponentWithCurrentNote,
} from './TransactionFunctions' } from './TransactionFunctions'
import { reloadFont } from './FontFunctions' import { reloadFont } from './FontFunctions'
import { NoteViewProps } from './NoteViewProps'
const MINIMUM_STATUS_DURATION = 400 const MINIMUM_STATUS_DURATION = 400
const TEXTAREA_DEBOUNCE = 100 const TEXTAREA_DEBOUNCE = 100
@@ -78,12 +78,7 @@ type State = {
rightResizerOffset: number rightResizerOffset: number
} }
interface Props { export class NoteView extends PureComponent<NoteViewProps, State> {
application: WebApplication
controller: NoteViewController
}
export class NoteView extends PureComponent<Props, State> {
readonly controller!: NoteViewController readonly controller!: NoteViewController
private statusTimeout?: NodeJS.Timeout private statusTimeout?: NodeJS.Timeout
@@ -101,7 +96,7 @@ export class NoteView extends PureComponent<Props, State> {
private editorContentRef: RefObject<HTMLDivElement> private editorContentRef: RefObject<HTMLDivElement>
constructor(props: Props) { constructor(props: NoteViewProps) {
super(props, props.application) super(props, props.application)
this.controller = props.controller this.controller = props.controller
@@ -217,7 +212,7 @@ export class NoteView extends PureComponent<Props, State> {
} }
} }
override componentDidUpdate(_prevProps: Props, prevState: State): void { override componentDidUpdate(_prevProps: NoteViewProps, prevState: State): void {
if ( if (
this.state.showProtectedWarning != undefined && this.state.showProtectedWarning != undefined &&
prevState.showProtectedWarning !== this.state.showProtectedWarning prevState.showProtectedWarning !== this.state.showProtectedWarning

View File

@@ -0,0 +1,8 @@
import { NoteViewController } from '@standardnotes/snjs'
import { WebApplication } from '@/UIModels/Application'
export interface NoteViewProps {
application: WebApplication
controller: NoteViewController
}

View File

@@ -1,4 +1,5 @@
import { ButtonType, sanitizeHtmlString, AlertService } from '@standardnotes/snjs' import { ButtonType, sanitizeHtmlString } from '@standardnotes/snjs'
import { AlertService } from '@standardnotes/services'
import { SKAlert } from '@standardnotes/stylekit' import { SKAlert } from '@standardnotes/stylekit'
/** @returns a promise resolving to true if the user confirmed, false if they canceled */ /** @returns a promise resolving to true if the user confirmed, false if they canceled */

View File

@@ -31,17 +31,13 @@ import { AbstractState } from './AbstractState'
import { SelectedItemsState } from './SelectedItemsState' import { SelectedItemsState } from './SelectedItemsState'
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
import { AppStateEvent } from './AppStateEvent' import { AppStateEvent } from './AppStateEvent'
import { EventSource } from './EventSource'
export type PanelResizedData = { export type PanelResizedData = {
panel: string panel: string
collapsed: boolean collapsed: boolean
} }
export enum EventSource {
UserInteraction,
Script,
}
type ObserverCallback = (event: AppStateEvent, data?: unknown) => Promise<void> type ObserverCallback = (event: AppStateEvent, data?: unknown) => Promise<void>
export class AppState extends AbstractState { export class AppState extends AbstractState {

View File

@@ -0,0 +1,4 @@
export enum EventSource {
UserInteraction,
Script,
}

View File

@@ -1,3 +1,4 @@
export { AppState, EventSource, PanelResizedData } from './AppState' export { AppState, PanelResizedData } from './AppState'
export * from './AppStateEvent' export * from './AppStateEvent'
export * from './EventSource'
export * from './PurchaseFlowPane' export * from './PurchaseFlowPane'

View File

@@ -25,4 +25,12 @@ module.exports = {
'^.+\\.(ts|tsx)?$': 'ts-jest', '^.+\\.(ts|tsx)?$': 'ts-jest',
'\\.svg$': 'svg-jest', '\\.svg$': 'svg-jest',
}, },
coverageThreshold: {
global: {
branches: 3,
functions: 5,
lines: 21,
statements: 22,
},
},
} }

View File

@@ -16,8 +16,7 @@
"setup": "bundle install && yarn install --frozen-lockfile && bundle exec rails assets:precompile && yarn bundle", "setup": "bundle install && yarn install --frozen-lockfile && bundle exec rails assets:precompile && yarn bundle",
"lint": "eslint --fix app/assets/javascripts", "lint": "eslint --fix app/assets/javascripts",
"tsc": "tsc --project app/assets/javascripts/tsconfig.json", "tsc": "tsc --project app/assets/javascripts/tsconfig.json",
"test": "jest --config app/assets/javascripts/jest.config.js", "test": "jest --config app/assets/javascripts/jest.config.js --coverage",
"test:coverage": "yarn test --coverage",
"prepare": "husky install", "prepare": "husky install",
"postinstall": "yarn run ncu -loglevel verbose --packageFile package.json", "postinstall": "yarn run ncu -loglevel verbose --packageFile package.json",
"upgrade:snjs": "ncu -u '@standardnotes/*' && yarn" "upgrade:snjs": "ncu -u '@standardnotes/*' && yarn"
@@ -73,8 +72,9 @@
"@standardnotes/components": "1.8.1", "@standardnotes/components": "1.8.1",
"@standardnotes/filepicker": "1.16.0", "@standardnotes/filepicker": "1.16.0",
"@standardnotes/icons": "^1.1.7", "@standardnotes/icons": "^1.1.7",
"@standardnotes/services": "^1.13.1",
"@standardnotes/sncrypto-web": "1.10.1", "@standardnotes/sncrypto-web": "1.10.1",
"@standardnotes/snjs": "2.113.2", "@standardnotes/snjs": "^2.113.2",
"@standardnotes/stylekit": "5.29.0", "@standardnotes/stylekit": "5.29.0",
"@zip.js/zip.js": "^2.4.10", "@zip.js/zip.js": "^2.4.10",
"mobx": "^6.5.0", "mobx": "^6.5.0",

View File

@@ -2402,7 +2402,7 @@
buffer "^6.0.3" buffer "^6.0.3"
libsodium-wrappers "^0.7.9" libsodium-wrappers "^0.7.9"
"@standardnotes/snjs@2.113.2": "@standardnotes/snjs@^2.113.2":
version "2.113.2" version "2.113.2"
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.113.2.tgz#efcd435c7e699397f94caa76273440ff56f3f91f" resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.113.2.tgz#efcd435c7e699397f94caa76273440ff56f3f91f"
integrity sha512-P93YfvJJJsntiILezwQUkx0ta5Amjd3Pte9+KkUFcLGBL1pkdtoY5lHc1oreWMElYuPTA+K9WrU6/tWa2OVa5A== integrity sha512-P93YfvJJJsntiILezwQUkx0ta5Amjd3Pte9+KkUFcLGBL1pkdtoY5lHc1oreWMElYuPTA+K9WrU6/tWa2OVa5A==