Skip to content

Commit

Permalink
test: add tests for cross module features
Browse files Browse the repository at this point in the history
  • Loading branch information
plinnegan committed Oct 4, 2024
1 parent 9d52ba6 commit 6b311b1
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 9 deletions.
60 changes: 59 additions & 1 deletion cypress/e2e/01-edit.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="cypress" />
/// <reference path="../support/index.d.ts" />
import testHtml from '../support/testHtmlContents.ts'
import { ViewMode } from "../support/interfaces"
import { styleMatch } from "../support/utils"

Expand Down Expand Up @@ -95,6 +96,30 @@ describe('Edit content as expected', () => {
cy.get('div.jodit-wysiwyg > ol > li').contains('Initial text')
})

it('Can insert tables', () => {
cy.get('span[aria-label="Insert table"] > button').click()
cy.get('div.jodit-popup div.jodit-form__container > div:nth-child(5) > span:nth-child(5)').click()
cy.get('div.jodit-wysiwyg > table').should('exist')
cy.get('div.jodit-wysiwyg > table').as('table')
cy.get('@table').find('> tbody').should('exist')
cy.get('@table').find('> tbody > tr').should('have.length', 5)
cy.get('@table').find('> tbody > tr:first-child > td').should('have.length', 5)
})

it('Can insert horizontal lines', () => {
cy.get('span[aria-label="Insert Horizontal Line"] > button').click()
cy.get('div.jodit-wysiwyg > hr').should('exist')
})

it('Can insert images', () => {
const imageLink = 'https://dhis2.org/wp-content/uploads/dhis2-logo-rgb-positive.svg'
cy.get('span[aria-label="Insert Image"] > button').click()
cy.get('input[placeholder="https://"]').type(imageLink)
cy.contains('Insert').click()
cy.get('div.jodit-wysiwyg > p > img').should('exist')
cy.get('div.jodit-wysiwyg > p > img').should('have.attr', 'src', imageLink)
})

it('Can insert links', () => {
const [link, linkText] = ['https://dhis2.org/', 'dhis2']
cy.get('span[ref="link"]').click()
Expand All @@ -117,8 +142,41 @@ describe('Edit content as expected', () => {
})
})

it('Supports use of the html editor', () => {
const textBasic = 'Best heading'
// No closing tag because the editor inserts it automatically
const htmlBasic = `<h1>${textBasic}</h1>`
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.ace_content').type(htmlBasic)
cy.get('div.ace_content').contains(htmlBasic)
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.jodit-wysiwyg').contains(textBasic)
})

it('Cleans invalid HTML on mode switch', () => {
const textContent = 'Something'
const invalidHtml = `<p>${textContent}</h1>`
const validHtml = `<p>${textContent}</p>`
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.ace_content').type(invalidHtml)
cy.get('div.ace_content').contains(invalidHtml)
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.jodit-wysiwyg').contains(textContent)
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.ace_content').contains(validHtml)
})

it('Removes scripts on mode switch', () => {
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.ace_content').type(testHtml.dirtyContentTags, {parseSpecialCharSequences: false})
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.jodit-wysiwyg').contains('Clean heading')
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.ace_content').should('not.contain', 'script')
})

afterEach(() => {
cy.removeWidgetItem()
cy.wait(1000)
})
})
})
77 changes: 77 additions & 0 deletions cypress/e2e/02-saving.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/// <reference types="cypress" />
/// <reference path="../support/index.d.ts" />
import testHtml from '../support/testHtmlContents.ts'
import { ViewMode } from "../support/interfaces"
import { getUrl } from '../support/utils.ts'

describe('Changes can be saved and content will be sanitized on save', () => {
const widgetId = 'Cypr3s5Test'
beforeEach(() => {
cy.setupWidgetItem('Initial text ', { viewMode: ViewMode.EDITING, widgetId })
cy.contains('Initial text').should('exist')
cy.contains('Start writing').should('not.exist')
})

it('Retains content changes on save', () => {
cy.get('div.jodit-wysiwyg').type(' new content!')
cy.contains('Save').click()
cy.visit(getUrl(ViewMode.EDITABLE, widgetId))
cy.get('#content').should('exist')
cy.get('#content').contains('new content!')
})

it('Sanitizes insecure tags on save', () => {
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.ace_content').type(testHtml.dirtyContentTags, {parseSpecialCharSequences: false})
cy.get('span[aria-label="Change mode"]').click()
cy.contains('Save').click()
cy.visit(getUrl(ViewMode.EDITABLE, widgetId))
cy.get('#content link').should('not.exist')
})

it('Sanitizes disallowed iframes on save', () => {
cy.get('span[aria-label="Change mode"]').click()
cy.get('div.ace_content').type(
testHtml.dirtyContentIframes, {parseSpecialCharSequences: false}
)
cy.get('span[aria-label="Change mode"]').click()
cy.contains('Save').click()
cy.visit(getUrl(ViewMode.EDITABLE, widgetId))
cy.get('#content iframe').each(($iframe) => {
cy.wrap($iframe).invoke('attr', 'src').should('match', /^(https:\/\/)?www.youtube.com/)
})
cy.get('#snackbar').should('exist')
cy.get('#snackbar').contains('is not currently an allowed domain for iframe content')
})

it('Shows no error for valid nested menu', () => {
cy.get('span[aria-label="Change mode"]').click()
cy.addNestedMenu(testHtml.nestedMenu)
cy.get('span[aria-label="Change mode"]').click()
cy.contains('Save').click()
cy.visit(getUrl(ViewMode.EDITABLE, widgetId))
cy.get('#snackbar').should('not.be.visible')
cy.get('#content').contains('Hello')
cy.contains('World').click()
cy.contains('World').should('have.class', 'selected')
cy.contains('What a Wonderful').click()
cy.contains('I see trees').click()
cy.contains('Of green').should('have.attr', 'onclick')
})

it('Shows error when invalid nested menu syntax is provided', () => {
const invalidNestedMenu = testHtml.nestedMenu.replace('</pre>', 'ExtraText</pre>')
cy.get('span[aria-label="Change mode"]').click()
cy.addNestedMenu(invalidNestedMenu)
cy.get('span[aria-label="Change mode"]').click()
cy.contains('Save').click()
cy.visit(getUrl(ViewMode.EDITABLE, widgetId))
cy.get('#content').contains('There was an error with the nested menu syntax:')
cy.get('#content').contains('ExtraText')
})

afterEach(() => {
cy.removeWidgetItem()
cy.wait(1000)
})
})
31 changes: 24 additions & 7 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
//
//
import {query} from '../../edit/src/services/init/initSaveButton.service'
import { SetupWidgetItemProps, ViewMode, } from './interfaces'
import { SetupWidgetItemProps, ViewMode } from './interfaces'
import { getUrl } from './utils'
const metaKey = Cypress.platform === 'darwin' ? 'cmd' : 'ctrl'

Cypress.Commands.add('setupWidgetItem', (content: string, options: SetupWidgetItemProps={}) => {
const widgetId = options?.widgetId ?? 'cypr3ssTe5t'
Expand All @@ -23,15 +25,30 @@ Cypress.Commands.add('setupWidgetItem', (content: string, options: SetupWidgetIt
return query(content, method, widgetId)
}
cy.wrap(setupWidget(content, widgetId)).then(() => {
const urlByViewMode = {
[ViewMode.DISPLAY]: `?dashboardItemId=${widgetId}`,
[ViewMode.EDITABLE]: `?dashboardItemId=${widgetId}#/xxx/edit`,
[ViewMode.EDITING]: `edit.html?dashboardItemId=${widgetId}`
}
cy.visit(urlByViewMode[viewMode])
cy.visit(getUrl(viewMode, widgetId))
})
})

Cypress.Commands.add('addNestedMenu', (contentRaw: string) => {
const allLines = contentRaw.split('\n').filter(Boolean).map(l => l.slice(4))
cy.get('div.ace_content').type('{enter}')
cy.get('div.ace_content')
.type(allLines[0])
.type(`{${metaKey}}{rightarrow}`)
.type('{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}')
cy.get('div.ace_content').type('{enter}')
const lines = allLines.slice(1)
let writeString = ''

for (const [idx, line] of lines.entries()) {
const prevLine = idx === 0 ? '' : lines[idx-1]
const lineSpaces = line.match(/^ +/)?.[0]?.length || 0
const prevLineSpaces = prevLine.match(/^ +/)?.[0]?.length || 0
writeString += `${'{backspace}'.repeat(prevLineSpaces/4)}${line}{enter}`
}
cy.get('div.ace_content').type(writeString)
})

Cypress.Commands.add('removeWidgetItem', (widgetId='cypr3ssTe5t') => {
query('', 'DELETE', widgetId)
cy.visit('/')
Expand Down
1 change: 1 addition & 0 deletions cypress/support/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
declare namespace Cypress {
interface Chainable {
setupWidgetItem(content: string, options?: {widgetId?: string, viewMode?: ViewMode}): void
addNestedMenu(content: string): void
removeWidgetItem(widgetId?: string): void
selectColor(params: {hexCode: string, isBackground?: boolean}): void
}
Expand Down
1 change: 1 addition & 0 deletions cypress/support/testHtmlContents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const testHtml: Record<string, string> = {
<h1 id="testHeading">Clean heading</h1>
<script>alert('Removed')</script>
<style>#testHeading {background-color: red;}</style>
<link rel="stylesheet" href="styles.css">
</div>
`,
dirtyContentIframes: `
Expand Down
11 changes: 11 additions & 0 deletions cypress/support/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { ViewMode } from "./interfaces"

export function styleMatch(styleStr: string): RegExp {
const escaped = styleStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return RegExp(`(^${escaped}| ${escaped})`)
}

export function getUrl(viewMode: ViewMode, widgetId: string): string {
const urlByViewMode = {
[ViewMode.DISPLAY]: `?dashboardItemId=${widgetId}`,
[ViewMode.EDITABLE]: `?dashboardItemId=${widgetId}#/xxx/edit`,
[ViewMode.EDITING]: `edit.html?dashboardItemId=${widgetId}`
}
return urlByViewMode[viewMode]
}
9 changes: 8 additions & 1 deletion shared/sanitizeContent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ export function sanitizeContent(input:string, allowedIframeDomains: string[]): [
let modifiedInput = input
const messages: string[] = []
for (const tag of badTags) {
modifiedInput = modifiedInput.replace(new RegExp(`<${tag}.+\/(${tag}|)>`), '')
if (tag === 'link') { // Link tags have no closing tag or / so need a different pattern
modifiedInput = modifiedInput.replace(/<link[^>]*>/mg, '')
} else {
modifiedInput = modifiedInput.replace(
new RegExp(`<${tag}[^>]*(\/>|>.*?<\/${tag}>)`, 'mg'), ''
)
}

}
if (input !== modifiedInput) {
messages.push(`Removed disallowed tags ${badTags.join(' & ')} are not allowed`)
Expand Down

0 comments on commit 6b311b1

Please sign in to comment.