aboutsummaryrefslogtreecommitdiffstats
path: root/cypress/support
diff options
context:
space:
mode:
Diffstat (limited to 'cypress/support')
-rw-r--r--cypress/support/commands.ts211
-rw-r--r--cypress/support/commonUtils.ts80
-rw-r--r--cypress/support/component-index.html4
-rw-r--r--cypress/support/component.ts94
-rw-r--r--cypress/support/cypress-component.d.ts17
-rw-r--r--cypress/support/cypress-e2e.d.ts64
-rw-r--r--cypress/support/e2e.ts33
-rw-r--r--cypress/support/utils/assertions.ts40
8 files changed, 394 insertions, 149 deletions
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index d214bb2b45e..ad486a8a8f7 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -1,77 +1,51 @@
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-/* eslint-disable n/no-unpublished-import */
-import axios from '@nextcloud/axios'
+// eslint-disable-next-line n/no-extraneous-import
+import axios from 'axios'
import { addCommands, User } from '@nextcloud/cypress'
import { basename } from 'path'
// Add custom commands
+import '@testing-library/cypress/add-commands'
+import 'cypress-if'
import 'cypress-wait-until'
addCommands()
-// Register this file's custom commands types
-declare global {
- // eslint-disable-next-line @typescript-eslint/no-namespace
- namespace Cypress {
- interface Chainable<Subject = any> {
- /**
- * Upload a file from the fixtures folder to a given user storage.
- * **Warning**: Using this function will reset the previous session
- */
- uploadFile(user: User, fixture?: string, mimeType?: string, target?: string): Cypress.Chainable<void>,
-
- /**
- * Upload a raw content to a given user storage.
- * **Warning**: Using this function will reset the previous session
- */
- uploadContent(user: User, content: Blob, mimeType: string, target: string): Cypress.Chainable<void>,
-
- /**
- * Reset the admin theming entirely.
- * **Warning**: Using this function will reset the previous session
- */
- resetAdminTheming(): Cypress.Chainable<void>,
-
- /**
- * Reset the user theming settings.
- * If provided, will clear session and login as the given user.
- * **Warning**: Providing a user will reset the previous session.
- */
- resetUserTheming(user?: User): Cypress.Chainable<void>,
-
- /**
- * Run an occ command in the docker container.
- */
- runOccCommand(command: string): Cypress.Chainable<void>,
- }
- }
-}
-
const url = (Cypress.config('baseUrl') || '').replace(/\/index.php\/?$/g, '')
Cypress.env('baseUrl', url)
/**
+ * Enable or disable a user
+ * TODO: standardize in @nextcloud/cypress
+ *
+ * @param {User} user the user to dis- / enable
+ * @param {boolean} enable True if the user should be enable, false to disable
+ */
+Cypress.Commands.add('enableUser', (user: User, enable = true) => {
+ const url = `${Cypress.config('baseUrl')}/ocs/v2.php/cloud/users/${user.userId}/${enable ? 'enable' : 'disable'}`.replace('index.php/', '')
+ return cy.request({
+ method: 'PUT',
+ url,
+ form: true,
+ auth: {
+ user: 'admin',
+ password: 'admin',
+ },
+ headers: {
+ 'OCS-ApiRequest': 'true',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ }).then((response) => {
+ cy.log(`Enabled user ${user}`, response.status)
+ return cy.wrap(response)
+ })
+})
+
+/**
* cy.uploadedFile - uploads a file from the fixtures folder
- * TODO: standardise in @nextcloud/cypress
+ * TODO: standardize in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {string} fixture the fixture file name, e.g. image1.jpg
@@ -80,50 +54,132 @@ Cypress.env('baseUrl', url)
*/
Cypress.Commands.add('uploadFile', (user, fixture = 'image.jpg', mimeType = 'image/jpeg', target = `/${fixture}`) => {
// get fixture
- return cy.fixture(fixture, 'base64').then(async file => {
- // convert the base64 string to a blob
- const blob = Cypress.Blob.base64StringToBlob(file, mimeType)
+ return cy.fixture(fixture, 'base64')
+ .then((file) => (
+ // convert the base64 string to a blob
+ Cypress.Blob.base64StringToBlob(file, mimeType)
+ ))
+ .then((blob) => cy.uploadContent(user, blob, mimeType, target))
+})
- cy.uploadContent(user, blob, mimeType, target)
- })
+Cypress.Commands.add('setFileAsFavorite', (user: User, target: string, favorite = true) => {
+ // eslint-disable-next-line cypress/unsafe-to-chain-command
+ cy.clearAllCookies()
+ .then(async () => {
+ try {
+ const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
+ const filePath = target.split('/').map(encodeURIComponent).join('/')
+ const response = await axios({
+ url: `${rootPath}${filePath}`,
+ method: 'PROPPATCH',
+ auth: {
+ username: user.userId,
+ password: user.password,
+ },
+ headers: {
+ 'Content-Type': 'application/xml',
+ },
+ data: `<?xml version="1.0"?>
+ <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
+ <d:set>
+ <d:prop>
+ <oc:favorite>${favorite ? 1 : 0}</oc:favorite>
+ </d:prop>
+ </d:set>
+ </d:propertyupdate>`,
+ })
+ cy.log(`Created directory ${target}`, response)
+ } catch (error) {
+ cy.log('error', error)
+ throw new Error('Unable to process fixture')
+ }
+ })
+})
+
+Cypress.Commands.add('mkdir', (user: User, target: string) => {
+ // eslint-disable-next-line cypress/unsafe-to-chain-command
+ return cy.clearCookies()
+ .then(async () => {
+ try {
+ const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
+ const filePath = target.split('/').map(encodeURIComponent).join('/')
+ const response = await axios({
+ url: `${rootPath}${filePath}`,
+ method: 'MKCOL',
+ auth: {
+ username: user.userId,
+ password: user.password,
+ },
+ })
+ cy.log(`Created directory ${target}`, response)
+ return response
+ } catch (error) {
+ cy.log('error', error)
+ throw new Error('Unable to create directory')
+ }
+ })
+})
+
+Cypress.Commands.add('rm', (user: User, target: string) => {
+ // eslint-disable-next-line cypress/unsafe-to-chain-command
+ cy.clearCookies()
+ .then(async () => {
+ try {
+ const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
+ const filePath = target.split('/').map(encodeURIComponent).join('/')
+ const response = await axios({
+ url: `${rootPath}${filePath}`,
+ method: 'DELETE',
+ auth: {
+ username: user.userId,
+ password: user.password,
+ },
+ })
+ cy.log(`delete file or directory ${target}`, response)
+ } catch (error) {
+ cy.log('error', error)
+ throw new Error('Unable to delete file or directory')
+ }
+ })
})
/**
* cy.uploadedContent - uploads a raw content
- * TODO: standardise in @nextcloud/cypress
+ * TODO: standardize in @nextcloud/cypress
*
* @param {User} user the owner of the file, e.g. admin
* @param {Blob} blob the content to upload
* @param {string} mimeType e.g. image/png
* @param {string} target the target of the file relative to the user root
*/
-Cypress.Commands.add('uploadContent', (user, blob, mimeType, target) => {
+Cypress.Commands.add('uploadContent', (user: User, blob: Blob, mimeType: string, target: string, mtime?: number) => {
cy.clearCookies()
- .then(async () => {
+ return cy.then(async () => {
const fileName = basename(target)
// Process paths
const rootPath = `${Cypress.env('baseUrl')}/remote.php/dav/files/${encodeURIComponent(user.userId)}`
const filePath = target.split('/').map(encodeURIComponent).join('/')
try {
- const file = new File([blob], fileName, { type: mimeType })
- await axios({
+ const file = new File([blob], fileName, { type: mimeType })
+ const response = await axios({
url: `${rootPath}${filePath}`,
method: 'PUT',
data: file,
headers: {
'Content-Type': mimeType,
+ 'X-OC-MTime': mtime ? `${mtime}` : undefined,
},
auth: {
username: user.userId,
password: user.password,
},
- }).then(response => {
- cy.log(`Uploaded content as ${fileName}`, response)
})
+ cy.log(`Uploaded content as ${fileName}`, response)
+ return response
} catch (error) {
cy.log('error', error)
- throw new Error(`Unable to process fixture`)
+ throw new Error('Unable to process fixture')
}
})
})
@@ -184,6 +240,9 @@ Cypress.Commands.add('resetUserTheming', (user?: User) => {
}
})
-Cypress.Commands.add('runOccCommand', (command: string) => {
- cy.exec(`docker exec --user www-data nextcloud-cypress-tests-server php ./occ ${command}`)
-}) \ No newline at end of file
+Cypress.Commands.add('userFileExists', (user: string, path: string) => {
+ user.replaceAll('"', '\\"')
+ path.replaceAll('"', '\\"').replaceAll(/^\/+/gm, '')
+ return cy.runCommand(`stat --printf="%s" "data/${user}/files/${path}"`, { failOnNonZeroExit: true })
+ .then((exec) => Number.parseInt(exec.stdout || '0'))
+})
diff --git a/cypress/support/commonUtils.ts b/cypress/support/commonUtils.ts
new file mode 100644
index 00000000000..8d02ace151b
--- /dev/null
+++ b/cypress/support/commonUtils.ts
@@ -0,0 +1,80 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { basename } from 'path'
+
+/**
+ * Get the header navigation bar
+ */
+export function getNextcloudHeader() {
+ return cy.get('#header')
+}
+
+/**
+ * Get user menu in the header navigation bar
+ */
+export function getNextcloudUserMenu() {
+ return getNextcloudHeader().find('#user-menu')
+}
+
+/**
+ * Get the user menu toggle in the header navigation bar
+ */
+export function getNextcloudUserMenuToggle() {
+ return getNextcloudUserMenu().find('.header-menu__trigger').should('have.length', 1)
+}
+
+/**
+ * Helper function ensure users and groups in this tests have a clean state
+ * Deletes all users (except admin) and groups
+ */
+export function clearState() {
+ // cleanup ignoring any failures
+ cy.runOccCommand('group:list --output=json').then(($result) => {
+ const groups = Object.keys(JSON.parse($result.stdout)).filter((name) => name !== 'admin')
+ groups.forEach((groupID) => cy.runOccCommand(`group:delete '${groupID}'`))
+ })
+
+ cy.runOccCommand('user:list --output=json').then(($result) => {
+ const users = Object.keys(JSON.parse($result.stdout)).filter((name) => name !== 'admin')
+ users.forEach((userID) => cy.runOccCommand(`user:delete '${userID}'`))
+ })
+}
+
+/**
+ * Install the test app
+ */
+export function installTestApp() {
+ const testAppPath = 'cypress/fixtures/testapp'
+ cy.runOccCommand('-V').then((output) => {
+ const version = output.stdout.match(/(\d\d+)\.\d+\.\d+/)?.[1]
+ cy.wrap(version).should('not.be.undefined')
+ getContainerName()
+ .then(containerName => {
+ cy.exec(`docker cp '${testAppPath}' ${containerName}:/var/www/html/apps`, { log: true })
+ cy.exec(`docker exec --workdir /var/www/html ${containerName} chown -R www-data:www-data /var/www/html/apps/testapp`)
+ })
+ cy.runCommand(`sed -i -e 's|-version=\\"[0-9]\\+|-version=\\"${version}|g' apps/testapp/appinfo/info.xml`)
+ cy.runOccCommand('app:enable --force testapp')
+ })
+}
+
+/**
+ * Remove the test app
+ */
+export function uninstallTestApp() {
+ cy.runOccCommand('app:remove testapp', { failOnNonZeroExit: false })
+ cy.runCommand('rm -fr apps/testapp/appinfo/info.xml')
+}
+
+/**
+ *
+ */
+export function getContainerName(): Cypress.Chainable<string> {
+ return cy.exec('pwd')
+ .then(({ stdout }) => {
+ return cy.wrap(`nextcloud-cypress-tests_${basename(stdout).replace(' ', '')}`)
+ })
+}
diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html
index ac6e79fd83d..e525b445373 100644
--- a/cypress/support/component-index.html
+++ b/cypress/support/component-index.html
@@ -1,4 +1,8 @@
<!DOCTYPE html>
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<html>
<head>
<meta charset="utf-8">
diff --git a/cypress/support/component.ts b/cypress/support/component.ts
index be4b8c94b1b..853609bb4dd 100644
--- a/cypress/support/component.ts
+++ b/cypress/support/component.ts
@@ -1,57 +1,45 @@
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { mount } from 'cypress/vue2'
-
-// Augment the Cypress namespace to include type definitions for
-// your custom command.
-// Alternatively, can be defined in cypress/support/component.d.ts
-// with a <reference path="./component" /> at the top of your spec.
-declare global {
- // eslint-disable-next-line @typescript-eslint/no-namespace
- namespace Cypress {
- interface Chainable {
- mount: typeof mount
- }
- }
-}
-
-// Example use:
-// cy.mount(MyComponent)
-Cypress.Commands.add('mount', (component, optionsOrProps) => {
- let instance = null
- const oldMounted = component?.mounted || false
-
- // Override the mounted method to expose
- // the component instance to cypress
- component.mounted = function() {
- // eslint-disable-next-line
- instance = this
- if (oldMounted) {
- oldMounted()
- }
- }
-
- // Expose the component with cy.get('@component')
- return mount(component, optionsOrProps).then(() => {
- return cy.wrap(instance).as('component')
+
+import '@testing-library/cypress/add-commands'
+import 'cypress-axe'
+
+// styles
+import '../../apps/theming/css/default.css'
+import '../../core/css/server.css'
+
+/* eslint-disable */
+import { mount } from '@cypress/vue2'
+
+Cypress.Commands.add('mount', (component, options = {}) => {
+ // Setup options object
+ options.extensions = options.extensions || {}
+ options.extensions.plugins = options.extensions.plugins || []
+ options.extensions.components = options.extensions.components || {}
+
+ return mount(component, options)
+})
+
+Cypress.Commands.add('mockInitialState', (app: string, key: string, value: unknown) => {
+ cy.document().then(($document) => {
+ const input = $document.createElement('input')
+ input.setAttribute('type', 'hidden')
+ input.setAttribute('id', `initial-state-${app}-${key}`)
+ input.setAttribute('value', btoa(JSON.stringify(value)))
+ $document.body.appendChild(input)
+ })
+})
+
+Cypress.Commands.add('unmockInitialState', (app?: string, key?: string) => {
+ cy.window().then(($window) => {
+ // @ts-expect-error internal value
+ delete $window._nc_initial_state
+ })
+
+ cy.document().then(($document) => {
+ $document.querySelectorAll('body > input[type="hidden"]' + (app ? `[id="initial-state-${app}-${key}"]` : ''))
+ .forEach((node) => $document.body.removeChild(node))
})
})
diff --git a/cypress/support/cypress-component.d.ts b/cypress/support/cypress-component.d.ts
new file mode 100644
index 00000000000..735db871e35
--- /dev/null
+++ b/cypress/support/cypress-component.d.ts
@@ -0,0 +1,17 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { mount } from '@cypress/vue2'
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ mount: typeof mount
+ mockInitialState: (app: string, key: string, value: unknown) => Cypress.Chainable<void>
+ unmockInitialState: (app?: string, key?: string) => Cypress.Chainable<void>
+ }
+ }
+}
diff --git a/cypress/support/cypress-e2e.d.ts b/cypress/support/cypress-e2e.d.ts
new file mode 100644
index 00000000000..97385ac070b
--- /dev/null
+++ b/cypress/support/cypress-e2e.d.ts
@@ -0,0 +1,64 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+// eslint-disable-next-line n/no-extraneous-import
+import type { AxiosResponse } from 'axios'
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
+ interface Chainable<Subject = any> {
+ /**
+ * Enable or disable a given user
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ enableUser(user: User, enable?: boolean): Cypress.Chainable<Cypress.Response<any>>,
+
+ /**
+ * Upload a file from the fixtures folder to a given user storage.
+ * **Warning**: Using this function will reset the previous session
+ */
+ uploadFile(user: User, fixture?: string, mimeType?: string, target?: string): Cypress.Chainable<AxiosResponse>,
+
+ /**
+ * Upload a raw content to a given user storage.
+ * **Warning**: Using this function will reset the previous session
+ */
+ uploadContent(user: User, content: Blob, mimeType: string, target: string, mtime?: number): Cypress.Chainable<AxiosResponse>,
+
+ /**
+ * Delete a file or directory
+ */
+ rm(user: User, target: string): Cypress.Chainable<AxiosResponse>,
+
+ /**
+ * Create a new directory
+ * **Warning**: Using this function will reset the previous session
+ */
+ mkdir(user: User, target: string): Cypress.Chainable<AxiosResponse>,
+
+ /**
+ * Set a file as favorite (or remove from favorite)
+ */
+ setFileAsFavorite(user: User, target: string, favorite?: boolean): Cypress.Chainable<void>,
+
+ /**
+ * Reset the admin theming entirely.
+ * **Warning**: Using this function will reset the previous session
+ */
+ resetAdminTheming(): Cypress.Chainable<void>,
+
+ /**
+ * Reset the user theming settings.
+ * If provided, will clear session and login as the given user.
+ * **Warning**: Providing a user will reset the previous session.
+ */
+ resetUserTheming(user?: User): Cypress.Chainable<void>,
+
+ userFileExists(user: string, path: string): Cypress.Chainable<number>
+ }
+ }
+}
diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts
index 4c1ddcc344a..65fb4b2a110 100644
--- a/cypress/support/e2e.ts
+++ b/cypress/support/e2e.ts
@@ -1,22 +1,15 @@
/**
- * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import './commands'
+import 'cypress-axe'
+import './commands.ts'
+
+// Remove with Node 22
+// Ensure that we can use `Promise.withResolvers` - works in browser but on Node we need Node 22+
+import 'core-js/actual/promise/with-resolvers.js'
+
+// Fix ResizeObserver loop limit exceeded happening in Cypress only
+// @see https://github.com/cypress-io/cypress/issues/20341
+Cypress.on('uncaught:exception', err => !err.message.includes('ResizeObserver loop limit exceeded'))
+Cypress.on('uncaught:exception', err => !err.message.includes('ResizeObserver loop completed with undelivered notifications'))
diff --git a/cypress/support/utils/assertions.ts b/cypress/support/utils/assertions.ts
new file mode 100644
index 00000000000..08b93b32e86
--- /dev/null
+++ b/cypress/support/utils/assertions.ts
@@ -0,0 +1,40 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { ZipReader } from '@zip.js/zip.js'
+/**
+ * Assert that a file contains a list of expected files
+ * @param expectedFiles List of expected filenames
+ * @example
+ * ```js
+ * cy.readFile('file', null, { ... })
+ * .should(zipFileContains(['file.txt']))
+ * ```
+ */
+export function zipFileContains(expectedFiles: string[]) {
+ return async (buffer: Buffer) => {
+ const blob = new Blob([buffer])
+ const zip = new ZipReader(blob.stream())
+ // check the real file names
+ const entries = (await zip.getEntries()).map((e) => e.filename).sort()
+ console.info('Zip contains entries:', entries)
+ expect(entries).to.deep.equal(expectedFiles.sort())
+ }
+}
+
+/**
+ * Check validity of an input element
+ * @param validity The expected validity message (empty string means it is valid)
+ * @example
+ * ```js
+ * cy.findByRole('textbox')
+ * .should(haveValidity(/must not be empty/i))
+ * ```
+ */
+export const haveValidity = (validity: string | RegExp) => {
+ if (typeof validity === 'string') {
+ return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.equal(validity)
+ }
+ return (el: JQuery<HTMLElement>) => expect((el.get(0) as HTMLInputElement).validationMessage).to.match(validity)
+}