diff --git a/.dockerignore b/.dockerignore index d24e919ae9a..bec82a253c6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,7 @@ * -!bin !docker -!lib -!package.json +!gulp +!test +!Gulpfile.js +!.publishrc +!*.tgz diff --git a/.eslintrc b/.eslintrc index 897b234ac27..38d0e84de38 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,11 +1,22 @@ { - "parser": "babel-eslint", - "extends": "eslint:recommended", + "parser": "@typescript-eslint/parser", + "env": { + "es6": true, + "node": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], "rules": { "no-alert": 2, "no-array-constructor": 2, "no-caller": 2, "no-catch-shadow": 2, + "no-console": 2, "no-eval": 2, "no-extend-native": 2, "no-extra-bind": 2, @@ -27,12 +38,14 @@ "no-return-assign": 2, "no-script-url": 2, "no-sequences": 2, - "no-shadow": 2, + "no-shadow": 0, + "@typescript-eslint/no-shadow": 2, "no-shadow-restricted-names": 2, "no-spaced-func": 2, "no-trailing-spaces": 2, "no-undef-init": 2, "no-unused-expressions": 2, + "no-var": 2, "no-with": 2, "camelcase": 2, "comma-spacing": 2, @@ -56,6 +69,7 @@ "no-underscore-dangle": 0, "no-unneeded-ternary": 2, "object-curly-spacing": [2, "always"], + "object-curly-newline": [2, { "ImportDeclaration": { "minProperties": 3, "consistent": true } }], "operator-assignment": [2, "always"], "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], "keyword-spacing": 2, @@ -75,19 +89,43 @@ } }], "radix": 2, - "no-extra-parens": 2, "new-cap": [2, { "capIsNew": false }], "space-before-function-paren": [2, "always"], - "no-use-before-define" : [2, "nofunc"], "handle-callback-err": 0, "linebreak-style": [2, "unix"], - "import/export": 2, - "import/no-duplicates": 2 - }, - "env": { - "node": true + "no-duplicate-imports": 2, + "comma-dangle": ["error", "always-multiline"], + "prefer-rest-params": 0, + "prefer-spread": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-extra-parens": 2, + "@typescript-eslint/no-use-before-define": [2, "nofunc"], + "@typescript-eslint/no-var-requires": [0], + "@typescript-eslint/explicit-function-return-type": "off", + "no-unused-vars": 0, + "@typescript-eslint/no-unused-vars": 2, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-this-alias": 0, + "@typescript-eslint/ban-ts-comment": 0, + "no-prototype-builtins": 0, + "@typescript-eslint/ban-types": [ + "error", + { + "types": { + "Function": false + } + } + ] }, - "plugins": [ - "import" + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@typescript-eslint/explicit-function-return-type": [2, { "allowExpressions": true }], + "@typescript-eslint/no-empty-function": 2, + "prefer-spread": 2, + "@typescript-eslint/ban-ts-ignore": 0 + } + } ] } diff --git a/.gitattributes b/.gitattributes index 0a279b6d0c4..4655cd50946 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,13 @@ * text=auto *.sh text eol=lf *.js text eol=lf +*.ts text eol=lf *.css text eol=lf *.html text eol=lf *.md text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.svg text eol=lf *.png binary *.ico binary diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 59f75de5c3e..b661ee83d2b 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,31 +1,38 @@ ### Are you requesting a feature or reporting a bug? + +### What is your Test Scenario? + -### What is the current behavior? +### What is the Current behavior? + -### What is the expected behavior? - +### What is the Expected behavior? + ### How would you reproduce the current behavior (if this is a bug)? - + #### Provide the test code and the tested page URL (if applicable) + + +Your website URL (or attach your complete example): -Tested page URL: - -Test code +Your complete test code (or attach your test files) ```js ``` - -### Specify your - -* operating system: -* testcafe version: -* node.js version: \ No newline at end of file +### Your Environment details: + +* testcafe version: +* node.js version: +* command-line arguments: +* browser name and version: +* platform and version: +* other: diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000000..2b0a3161f6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,159 @@ +name: Bug Report +description: Submit the behavior you consider invalid +labels: ["TYPE: bug"] +body: + - type: markdown + attributes: + value: | + Fill the template. Share as much information about the bug as you can. Create a minimal working example that allows the TestCafe team to reliably reproduce the bug. + + Use the latest [TestCafe](https://github.com/DevExpress/testcafe/releases) version - it includes more bug fixes. + + If your tests run in Chromium-based browsers, turn [native automation](https://testcafe.io/documentation/404237/guides/intermediate-guides/native-automation-mode) on and off, to confirm that it doesn't affect the issue. + + Before you submit an issue, please check [our GitHub repository](https://github.com/DevExpress/testcafe/issues) for similar tickets. This may save your time (and ours). + + - type: textarea + id: scenario + attributes: + label: What is your Scenario? + description: > + Describe what you'd like to do. + validations: + required: true + + - type: textarea + id: current + attributes: + label: What is the Current behavior? + description: > + Describe the behavior you see and consider invalid. + validations: + required: true + + - type: textarea + id: expected + attributes: + label: What is the Expected behavior? + description: > + Describe what you expected to happen. + validations: + required: true + + - type: textarea + id: url + attributes: + label: What is the public URL of the test page? (attach your complete example) + description: | + The TestCafe team needs to be able to reproduce the bug you encountered. Include a **public** test page URL in your example. Do not share any private data, such as the secret address of your staging server, or your access credentials. Refer to this article to learn how to create an example: [How To: Create a Minimal Working Example When You Submit an Issue](https://testcafe.io/402636/faq#how-to-create-a-minimal-working-example-when-you-submit-an-issue). + validations: + required: true + + - type: textarea + id: testcase + attributes: + label: What is your TestCafe test code? + description: > + Paste your complete test code including all the referenced modules, if any. + validations: + required: true + + - type: textarea + id: config + attributes: + label: Your complete configuration file + description: > + Paste your complete configuration file (e.g., .testcaferc.js or .testcaferc.json). + validations: + required: false + + - type: textarea + id: report + attributes: + label: Your complete test report + description: > + Paste the complete test report here (even if it is huge). + validations: + required: false + + - type: textarea + id: screenhots + attributes: + label: Screenshots + description: > + If applicable, attach screenshots to help explain the issue. + validations: + required: false + + - type: textarea + id: steps + attributes: + label: Steps to Reproduce + description: > + Describe what we should do to reproduce the behavior you encountered. + value: | + 1. + 2. + 3. + validations: + required: true + + - type: markdown + id: environment + attributes: + value: > + Your Environment details: + + - type: input + id: version + attributes: + label: TestCafe version + description: > + Run `testcafe -v` + validations: + required: true + + - type: input + id: nodejs + attributes: + label: Node.js version + description: > + Run `node -v` + validations: + required: false + + - type: input + id: cmdline + attributes: + label: Command-line arguments + description: > + Example: testcafe edge,chrome -e test.js + validations: + required: true + + - type: input + id: browser + attributes: + label: Browser name(s) and version(s) + description: > + Example: Edge 116, Chrome 116, Firefox 117, etc. + validations: + required: false + + - type: input + id: platform + attributes: + label: Platform(s) and version(s) + description: > + Example: macOS 10.14, Windows, Linux Ubuntu 18.04.1, iOS 13 + validations: + required: false + + - type: textarea + id: other + attributes: + label: Other + description: > + Any notes you consider important + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..3ba13e0cec6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000000..4869862c146 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,46 @@ +name: Feature Request +description: Share your ideas for this project +labels: ["TYPE: enhancement"] +body: + - type: markdown + attributes: + value: | + If you have an idea you think might be useful for others, please share as much detail as possible in the sections below. + + Before submitting an issue, please check [CONTRIBUTING.md](https://github.com/DevExpress/testcafe/blob/master/CONTRIBUTING.md) and existing issues in [this repository](https://github.com/DevExpress/testcafe/issues) in case a similar issue exists. This may save your time (and ours). + + - type: textarea + id: scenario + attributes: + label: What is your Scenario? + description: | + Describe what youd like to do. + validations: + required: true + + - type: textarea + id: suggestion + attributes: + label: "What are you suggesting?" + description: > + Describe the solution you'd like to propose and how it may help in your scenario. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: "What alternatives have you considered?" + description: > + Describe any alternative solutions or features you've considered if any. + validations: + required: false + + - type: textarea + id: context + attributes: + label: "Additional context" + description: > + Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml new file mode 100644 index 00000000000..f11f2ad5e04 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yaml @@ -0,0 +1,24 @@ +name: Question +description: Route your questions to StackOverflow +labels: ["TYPE: question"] + +body: + - type: markdown + attributes: + value: | + For TestCafe API, usage and configuration inquiries, we recommend [StackOverflow](https://stackoverflow.com/questions/ask?tags=testcafe) - an amazing platform for users to ask and answer questions. + + We try to keep the GitHub issues tracker for bugs and feature requests only (see also: [Contributing](https://github.com/DevExpress/testcafe#contributing)). + + You may also find answers in our up-to-date [documentation](https://testcafe.io/documentation/402635/getting-started) and [answered questions](https://stackoverflow.com/questions/tagged/testcafe) on StackOverflow. This may save your time. + + If you have already looked there and have not found an appropriate answer, [file a new question on StackOverflow](https://stackoverflow.com/questions/ask?tags=testcafe). + + - type: input + id: url + attributes: + label: File a new question on StackOverflow + value: | + https://stackoverflow.com/questions/ask?tags=testcafe + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..54c940a57b4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + + +## Purpose +_Describe the problem you want to address or the feature you want to implement._ + +## Approach +_Describe how your changes address the issue or implement the desired functionality in as much detail as possible._ + +## References +_Provide a link to the existing issue(s), if any._ + +## Pre-Merge TODO +- [ ] Write tests for your proposed changes +- [ ] Make sure that existing tests do not fail diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 00000000000..6cc435a2af5 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,129 @@ +# EDITS SHOULD BE SUBMITTED TO DevExpress/testcafe-build-system/config/labels.yml +# Configuration for Label Actions - https://github.com/dessant/label-actions + +? 'TYPE: question' +: + # Post a comment + comment: | + Thank you for your inquiry. It looks like you're asking a question. We use GitHub to track bug reports and enhancement requests (see [Contributing](https://github.com/DevExpress/testcafe#contributing)). Address your question to the TestCafe community on [StackOverflow](https://stackoverflow.com/questions/ask?tags=testcafe) instead. + + If you encountered a bug, [open a new issue](https://github.com/DevExpress/testcafe/issues/new?template=bug-report.md), and follow the "bug report" template. Thank you in advance. + unlabel: 'STATE: Need response' + close: true + +? 'STATE: Non-latest version' +: + # Post a comment + comment: | + Thank you for submitting a bug report. It looks like you're using an outdated version of TestCafe. Every TestCafe update contains bug fixes and enhancements. Install [the latest version](https://github.com/DevExpress/testcafe/releases/latest) of TestCafe and see if you can reproduce the bug. We look forward to your response. + label: 'STATE: Need clarification' + unlabel: + - 'STATE: Non-latest version' + - 'STATE: Need response' + +? 'STATE: Need simple sample' +: + # Post a comment + comment: | + Thank you for submitting a bug report. We would love to help you investigate the issue. Please share a *simple* code example that reliably reproduces the bug. For more information, read the following article: [How To Create a Minimal Working Example When You Submit an Issue](https://testcafe.io/documentation/402636/faq/general-info#how-to-create-a-minimal-working-example-when-you-submit-an-issue). We look forward to your response. + label: 'STATE: Need clarification' + unlabel: + - 'STATE: Need simple sample' + - 'STATE: Need response' + +? 'STATE: Need access confirmation' +: + # Post a comment + comment: | + Thank you for submitting a bug report. We would love to help you investigate the issue. Unfortunately, we cannot reproduce the bug, because your code example accesses a web resource that requires authentication. + + Please create a [Minimal Example](https://testcafe.io/documentation/402636/faq/general-info#how-to-create-a-minimal-working-example-when-you-submit-an-issue) that works locally or without authentication. Do not share any private data - the DevExpress support team cannot access private resources. We look forward to your response. + + label: 'STATE: Need clarification' + unlabel: + - 'STATE: Need access confirmation' + - 'STATE: Need response' + +? 'STATE: Incomplete template' +: + # Post a comment + comment: | + Thank you for submitting a bug report. The information you shared is not sufficient to determine the cause of the issue. Please create a new GitHub ticket and fill every section of the "bug report" template. Include the framework's version number, and don't forget to share a [Minimal Working Example](https://testcafe.io/documentation/402636/faq/general-info#how-to-create-a-minimal-working-example-when-you-submit-an-issue) that reliably reproduces the issue. + unlabel: + - 'STATE: Incomplete template' + - 'STATE: Need response' + close: true + +? 'STATE: No updates' +: + # Post a comment + comment: | + No updates yet. Once we make more progress, we will leave a comment. + unlabel: + - 'STATE: No updates' + - 'STATE: Need response' + +? 'STATE: No estimations' +: + # Post a comment + comment: | + Personal predictions can be unreliable, so we are not ready to give you an ETA. Once we make more progress, we will leave a comment. + unlabel: + - 'STATE: No estimations' + - 'STATE: Need response' + +? 'STATE: Outdated proposal' +: + # Post a comment + comment: | + The TestCafe team has yet to allocate any resources for the development of this capability. We cannot give you an ETA on its completion. If this capability is important for you, please submit a Pull Request with an implementation. See the [Сontribution guide](https://github.com/DevExpress/testcafe/blob/master/CONTRIBUTING.md) for more information. + unlabel: + - 'STATE: Outdated proposal' + - 'STATE: Need response' + +? 'STATE: Outdated issue' +: + # Post a comment + comment: | + When the TestCafe team decides which issues to address, it evaluates their severity, as well as the number of affected users. It appears that the issue you raised is an edge case. + + If this issue is important for you, please submit a Pull Request with a fix. See the [Сontribution guide](https://github.com/DevExpress/testcafe/blob/master/CONTRIBUTING.md) for more information. + unlabel: + - 'STATE: Outdated issue' + - 'STATE: Need response' + +? 'STATE: No workarounds' +: + # Post a comment + comment: | + There are no workarounds at the moment. We'll leave a comment if we discover a workaround, or fix the bug. + unlabel: + - 'STATE: No workarounds' + - 'STATE: Need response' + +? 'STATE: PR Review Pending' +: + # Post a comment + comment: | + Thank you for your contribution to TestCafe. When a member of the TestCafe team becomes available, they will review this PR. + unlabel: + - 'STATE: PR Review Pending' + - 'STATE: Need response' + +? 'STATE: Issue accepted' +: + # Post a comment + comment: | + We appreciate you taking the time to share information about this issue. We reproduced the bug and added this ticket to our internal task queue. We'll update this thread once we have news. + unlabel: + - 'STATE: Issue accepted' + - 'STATE: Need response' + +? 'STATE: Enhancement accepted' +: + # Post a comment + comment: | + Thank you for bringing this enhancement to our attention. We will be happy to look into it. We'll update this thread once we have news. If we do not publish any new comments, it's safe to assume that there are no new updates. + unlabel: + - 'STATE: Enhancement accepted' + - 'STATE: Need response' diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 00000000000..c18f0ba125b --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,44 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 10 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: + - "STATE: Need response" + - "STATE: Need clarification" + - "STATE: Stale" + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: "STATE: Auto-locked" + +# Comment to post before locking. Set to `false` to disable +lockComment: > + This thread has been automatically locked since it is closed and + there has not been any recent activity. Please open a new issue + for related [bugs](https://github.com/DevExpress/testcafe/issues/new?template=bug-report.md) + or [feature requests](https://github.com/DevExpress/testcafe/issues/new?template=feature_request.md). + We recommend you ask TestCafe API, usage and configuration inquiries + on [StackOverflow](https://stackoverflow.com/questions/ask?tags=testcafe). + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: true + +# Limit to only `issues` or `pulls` +only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000000..27bcee3fbf1 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,4 @@ +template: | + ## What’s Changed + + $CHANGES diff --git a/.github/scripts/security-checker.mjs b/.github/scripts/security-checker.mjs new file mode 100644 index 00000000000..0f31c40ff8a --- /dev/null +++ b/.github/scripts/security-checker.mjs @@ -0,0 +1,236 @@ +const STATES = { + open: 'open', + closed: 'closed', +}; + +const LABELS = { + dependabot: 'dependabot', + codeq: 'codeql', + security: 'security notification', +}; + +const ALERT_TYPES = { + dependabot: 'dependabot', + codeq: 'codeql', +} + +class SecurityChecker { + constructor(github, context, issueRepo) { + this.github = github; + this.issueRepo = issueRepo; + this.context = { + owner: context.repo.owner, + repo: context.repo.repo, + }; + } + + async check () { + const dependabotAlerts = await this.getDependabotAlerts(); + const codeqlAlerts = await this.getCodeqlAlerts(); + const existedIssues = await this.getExistedIssues(); + + this.alertDictionary = this.createAlertDictionary(existedIssues); + + await this.closeSpoiledIssues(); + await this.createDependabotlIssues(dependabotAlerts); + await this.createCodeqlIssues(codeqlAlerts); + } + + async getDependabotAlerts () { + const { data } = await this.github.rest.dependabot.listAlertsForRepo({ state: STATES.open, ...this.context }); + + return data; + } + + async getCodeqlAlerts () { + try { + const { data } = await this.github.rest.codeScanning.listAlertsForRepo({ state: STATES.open, ...this.context }); + + return data; + } + catch (e) { + if (e.message.includes('no analysis found') || e.message.includes('Advanced Security must be enabled for this repository to use code scanning')) + return []; + + throw e; + } + } + + async getExistedIssues () { + const { data: existedIssues } = await this.github.rest.issues.listForRepo({ + owner: this.context.owner, + repo: this.issueRepo, + labels: [LABELS.security], + state: STATES.open, + }); + + return existedIssues; + } + + createAlertDictionary (existedIssues) { + return existedIssues.reduce((res, issue) => { + const [, url, type] = issue.body.match(/(https:.*\/(dependabot|code-scanning)\/(\d+))/); + + if (!url) + return res; + + if (type === ALERT_TYPES.dependabot) { + const [, cveId] = issue.body.match(/CVE ID:\s*`(.*)`/); + const [, ghsaId] = issue.body.match(/GHSA ID:\s*`(.*)`/); + + res.set(issue.title, { issue, type, cveId, ghsaId }); + } + else + res.set(issue.title, { issue, type }); + + return res; + }, new Map()); + } + + async closeSpoiledIssues () { + const regExpAlertNumbers = new RegExp(`(?<=\`${this.context.repo}\` - https:.*/dependabot/)\\d+`,'g'); + + for (const alert of this.alertDictionary.values()) { + + if (alert.type === ALERT_TYPES.dependabot) { + const alertNumbers = alert.issue.body.match(regExpAlertNumbers); + + if (!alertNumbers) + continue; + + const updates = {}; + let changedBody = alert.issue.body; + + for (let alertNumber of alertNumbers) { + const isAlertOpened = await this.isDependabotAlertOpened(alertNumber); + + if (isAlertOpened) + continue; + + changedBody = changedBody.replace(new RegExp(`\\[ \\](?= \`${this.context.repo}\` - https:.*/dependabot/${alertNumber})`), '[x]'); + } + + updates.body = changedBody; + updates.state = !changedBody.match(/\[ \]/) ? STATES.closed : STATES.open; + updates.issue_number = alert.issue.number; + + await this.updateIssue(updates); + } + } + } + + async isDependabotAlertOpened (alertNumber) { + const alert = await this.getDependabotAlertInfo(alertNumber); + + return alert.state === STATES.open; + } + + async getDependabotAlertInfo (alertNumber) { + try { + const { data } = await this.github.rest.dependabot.getAlert({ alert_number: alertNumber, ...this.context }); + + return data; + } + catch (e) { + if (e.message.includes('No alert found for alert number')) + return {}; + + throw e; + } + } + + async updateIssue (updates) { + return this.github.rest.issues.update({ + owner: this.context.owner, + repo: this.issueRepo, + ...updates, + }); + } + + + async createDependabotlIssues (dependabotAlerts) { + for (const alert of dependabotAlerts) { + if (this.needAddAlertToIssue(alert)) { + await this.addAlertToIssue(alert); + } + else if (this.needCreateIssue(alert)) { + await this.createIssue({ + labels: [LABELS.dependabot, LABELS.security, alert.dependency.scope], + originRepo: this.context.repo, + summary: alert.security_advisory.summary, + description: alert.security_advisory.description, + link: alert.html_url, + issuePackage: alert.dependency.package.name, + cveId: alert.security_advisory.cve_id, + ghsaId: alert.security_advisory.ghsa_id, + }); + } + } + } + + needAddAlertToIssue (alert) { + const regExpAlertNumber = new RegExp(`(?<=\`${this.context.repo}\` - https:.*/dependabot/)${alert.html_url.match(/(?<=https:.*\/)\d+/)}`); + const existedIssue = this.alertDictionary.get(alert.security_advisory.summary); + const alertNumber = existedIssue?.issue.body.match(regExpAlertNumber); + const isAlertExisted = existedIssue?.issue.body.includes(`\`${this.context.repo}\``); + + return existedIssue + && existedIssue.cveId === alert.security_advisory.cve_id + && existedIssue.ghsaId === alert.security_advisory.ghsa_id + && (!isAlertExisted || (isAlertExisted && !alertNumber)); + } + + async addAlertToIssue (alert) { + const updates = {}; + const { issue } = this.alertDictionary.get(alert.security_advisory.summary); + + updates.issue_number = issue.number; + updates.body = issue.body.replace(/(?<=Repositories:)[\s\S]*?(?=####|$)/g, (match) => { + return match + `- [ ] \`${this.context.repo}\` - ${alert.html_url}\n`; + }); + + await this.updateIssue(updates); + } + + async createCodeqlIssues (codeqlAlerts) { + for (const alert of codeqlAlerts) { + if (!this.needCreateIssue(alert, false)) + continue; + + await this.createIssue({ + labels: [LABELS.codeql, LABELS.security], + originRepo: this.context.repo, + summary: alert.rule.description, + description: alert.most_recent_instance.message.text, + link: alert.html_url, + }, false); + } + } + + needCreateIssue (alert, isDependabotAlert = true) { + const dictionaryKey = isDependabotAlert ? alert.security_advisory.summary : `[${this.context.repo}] ${alert.rule.description}`; + + return !this.alertDictionary.get(dictionaryKey) && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24; + } + + async createIssue ({ labels, originRepo, summary, description, link, issuePackage = '', cveId, ghsaId }, isDependabotAlert = true) { + const title = isDependabotAlert ? `${summary}` : `[${originRepo}] ${summary}`; + let body = '' + + `#### Repositories:\n` + + `- [ ] \`${originRepo}\` - ${link}\n` + + (issuePackage ? `#### Package: \`${issuePackage}\`\n` : '') + + `#### Description:\n` + + `${description}\n`; + + if (isDependabotAlert) + body += `\n#### CVE ID: \`${cveId}\`\n#### GHSA ID: \`${ghsaId}\``; + + return this.github.rest.issues.create({ + title, body, labels, + owner: this.context.owner, + repo: this.issueRepo, + }); + } +} + +export default SecurityChecker; diff --git a/.github/workflows/check-security-alerts.yml b/.github/workflows/check-security-alerts.yml new file mode 100644 index 00000000000..e4928b8e797 --- /dev/null +++ b/.github/workflows/check-security-alerts.yml @@ -0,0 +1,36 @@ +name: Check security alerts + +on: + schedule: + - cron: "30 1 * * *" + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: latest + - uses: actions/github-script@v7 + with: + github-token: ${{ secrets.ACTIVE_TOKEN }} + script: | + const {default: SecurityChecker} = await import('${{ github.workspace }}/.github/scripts/security-checker.mjs') + + const securityChecker = new SecurityChecker(github, context, '${{secrets.SECURITY_ISSUE_REPO}}'); + + await securityChecker.check(); + + keepalive-job: + name: Keepalive Workflow + if: ${{ always() }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: gautamkrishnar/keepalive-workflow@v2 + with: + gh_token: ${{ secrets.ACTIVE_TOKEN }} diff --git a/.github/workflows/deploy-to-artifacts.yml b/.github/workflows/deploy-to-artifacts.yml new file mode 100644 index 00000000000..8514f843b5e --- /dev/null +++ b/.github/workflows/deploy-to-artifacts.yml @@ -0,0 +1,157 @@ +name: Deploy To Artifacts + +on: + workflow_dispatch: + inputs: + sha: + description: 'The commit ref or SHA' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + base_sha: + description: 'The base commit SHA' + +jobs: + build: + runs-on: ubuntu-latest + outputs: + sha: ${{steps.prep.outputs.sha}} + steps: + - uses: DevExpress/testcafe-build-system/actions/set-status@main + with: + status: 'pending' + + - name: Build Info + run: | + echo "SHA: ${{ github.event.inputs.sha }}" + echo "Merged SHA: ${{ github.event.inputs.merged_sha }}" + echo "Deployment run ID: ${{ github.run_id }}" + - id: prep + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + core.setOutput('sha', context.payload.inputs.merged_sha || context.payload.inputs.sha); + - uses: actions/checkout@v3 + with: + ref: ${{steps.prep.outputs.sha}} + - run: | + npm i + npx gulp build + npm pack + - id: package-name + uses: actions/github-script@v6 + with: + script: | + const { name, version } = require(require('path').join(process.env.GITHUB_WORKSPACE, 'package.json')); + + core.setOutput('packageName', `${name}-${version}`); + core.setOutput('imageName', `${name}/${name}:${version}`); + + - uses: actions/upload-artifact@v4 + with: + name: npm + path: | + ${{steps.package-name.outputs.packageName}}.tgz + - run: | + npx gulp docker-build + docker save -o ${{steps.package-name.outputs.packageName}}.tar ${{steps.package-name.outputs.imageName}} + - uses: actions/upload-artifact@v4 + with: + name: docker + path: ${{steps.package-name.outputs.packageName}}.tar + + - uses: DevExpress/testcafe-build-system/actions/set-status@main + if: failure() || cancelled() + with: + status: 'failure' + + changes: + # TODO: currently cannot generate a list of changes after rebase + runs-on: ubuntu-latest + steps: + - uses: DevExpress/testcafe-build-system/actions/set-status@main + with: + status: 'pending' + + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.sha }} + fetch-depth: 0 + - run: | + git reset --soft `git merge-base "${{github.event.inputs.base_sha}}" HEAD` + git diff --name-only --cached > changes.txt + - uses: actions/upload-artifact@v4 + with: + name: changes + path: changes.txt + + - uses: DevExpress/testcafe-build-system/actions/set-status@main + if: failure() || cancelled() + with: + status: 'failure' + + test: + needs: [build, changes] + runs-on: ubuntu-latest + environment: CI + steps: + - uses: actions/download-artifact@v4 + with: + name: changes + - uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + function getInputs () { + const { sha, merged_sha } = context.payload.inputs; + + return { + ...merged_sha ? { merged_sha } : {}, + sha, + deploy_run_id: '${{github.run_id}}' + } + } + + async function dispatchWorkflow (workflowName) { + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'master', + workflow_id: workflowName, + inputs: getInputs() + }); + } + + // TODO: Optimize by running only necessary tests for corresponding changes + const fileList = require('fs').readFileSync('changes.txt').toString().split('\n').filter(line => line); + + const tasks = []; + + tasks.push('test-client-desktop.yml'); + tasks.push('test-client-mobile.yml'); + tasks.push('test-functional-docker.yml'); + tasks.push('test-functional-local-safari.yml'); + tasks.push('test-functional-local-esm.yml'); + tasks.push('test-functional-local-chrome.yml'); + tasks.push('test-functional-local-edge.yml'); + tasks.push('test-functional-local-firefox.yml'); + tasks.push('test-functional-local-multiple-windows.yml'); + tasks.push('test-functional-local-multiple-windows-na.yml'); + tasks.push('test-functional-local-native-automation.yml'); + tasks.push('test-functional-local-headed-browsers.yml'); + tasks.push('test-functional-local-legacy.yml'); + tasks.push('test-functional-remote-mobile.yml'); + tasks.push('test-server-docker.yml'); + tasks.push('test-server-minimal.yml'); + tasks.push('test-server-latest.yml'); + tasks.push('license-check.yml'); + + await Promise.all(tasks.map(task => dispatchWorkflow(task))); + + - uses: DevExpress/testcafe-build-system/actions/set-status@main + if: always() + with: + status: ${{ fromJSON('["failure", "success"]')[job.status == 'success'] }} + artifacts-path: ${{ fromJSON('["", "#artifacts"]')[job.status == 'success'] }} diff --git a/.github/workflows/handle-labels.yml b/.github/workflows/handle-labels.yml new file mode 100644 index 00000000000..30b2f95ac5f --- /dev/null +++ b/.github/workflows/handle-labels.yml @@ -0,0 +1,13 @@ +name: 'Label Actions' + +on: + issues: + types: [labeled, unlabeled] + pull_request_target: + types: [labeled, unlabeled] + +jobs: + reaction: + runs-on: ubuntu-latest + steps: + - uses: DevExpress/testcafe-build-system/actions/handle-labels@main diff --git a/.github/workflows/handle-stale.yml b/.github/workflows/handle-stale.yml new file mode 100644 index 00000000000..6d5f26d9fa8 --- /dev/null +++ b/.github/workflows/handle-stale.yml @@ -0,0 +1,33 @@ +name: "Mark stale issues and pull requests" +on: + schedule: + - cron: "30 1 * * *" + workflow_dispatch: +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3 + with: + stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for a long period. It will be closed and archived if no further activity occurs. However, we may return to this issue in the future. If it still affects you or you have any additional information regarding it, please leave a comment and we will keep it open." + stale-pr-message: "This pull request has been automatically marked as stale because it has not had any activity for a long period. It will be closed and archived if no further activity occurs. However, we may return to this pull request in the future. If it is still relevant or you have any additional information regarding it, please leave a comment and we will keep it open." + close-issue-message: "We're closing this issue after a prolonged period of inactivity. If it still affects you, please add a comment to this issue with up-to-date information. Thank you." + close-pr-message: "We're closing this pull request after a prolonged period of inactivity. If it is still relevant, please ask for this pull request to be reopened. Thank you." + stale-issue-label: "STATE: Stale" + stale-pr-label: "STATE: Stale" + days-before-stale: 180 + days-before-close: 10 + exempt-issue-labels: "AREA: docs,FREQUENCY: critical,FREQUENCY: level 2,HELP WANTED,!IMPORTANT!,STATE: Need clarification,STATE: Need response,STATE: won't fix,support center" + exempt-pr-labels: "AREA: docs,FREQUENCY: critical,FREQUENCY: level 2,HELP WANTED,!IMPORTANT!,STATE: Need clarification,STATE: Need response,STATE: won't fix,support center" + + keepalive-job: + name: Keepalive Workflow + if: ${{ always() }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: gautamkrishnar/keepalive-workflow@v2 + with: + gh_token: ${{ secrets.ACTIVE_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/helper-rebase.yml b/.github/workflows/helper-rebase.yml new file mode 100644 index 00000000000..5afb73bb84d --- /dev/null +++ b/.github/workflows/helper-rebase.yml @@ -0,0 +1,23 @@ +name: Helper (Rebase) + +on: + issue_comment: + types: [created] + +env: + GITHUB_TOKEN: ${{ secrets.ACTIVE_TOKEN }} + +jobs: + rebase: + name: Rebase + if: github.event.issue.pull_request && (contains(github.event.comment.body, '/rebase') || contains(github.event.comment.body, '\rebase')) + runs-on: ubuntu-latest + steps: + - name: Checkout the latest code + uses: actions/checkout@v3 + with: + token: ${{ secrets.ACTIVE_TOKEN }} + fetch-depth: 0 # otherwise, you will fail to push refs to dest repo + - name: Automatic Rebase + uses: cirrus-actions/rebase@1.4 + diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml new file mode 100644 index 00000000000..a33c9e9faee --- /dev/null +++ b/.github/workflows/license-check.yml @@ -0,0 +1,38 @@ +name: Check Licenses + +on: + workflow_dispatch: + inputs: + sha: + description: "The test commit SHA or ref" + required: true + default: "master" + merged_sha: + description: "The merge commit SHA" + deploy_run_id: + description: "The ID of a deployment workspace run with artifacts" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: latest + + - name: Install dependencies + run: npm install + + - name: Run Gulp build + run: npx gulp build + + - name: Pack the application + run: npm pack + + - name: Install the application and check licenses + run: mkdir temp && cd temp && npm init -y && npm install ../*.tgz && npx gulp check-licenses diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 00000000000..e8072452c30 --- /dev/null +++ b/.github/workflows/no-response.yml @@ -0,0 +1,27 @@ +name: No Response + +# Both `issue_comment` and `scheduled` event types are required for this Action +# to work properly. +on: + issue_comment: + types: [created] + schedule: + # Schedule for five minutes after the hour, every hour + - cron: '5 * * * *' + +jobs: + noResponse: + runs-on: ubuntu-latest + steps: + - uses: lee-dohm/no-response@v0.5.0 + with: + token: ${{ github.token }} + daysUntilClose: 10 + responseRequiredLabel: "STATE: Need clarification" + closeComment: > + This issue was automatically closed because there was no response + to our request for more information from the original author. + Currently, we don't have enough information to take action. + Please reach out to us if you find the necessary information + and are able to share it. We are also eager to know if you resolved + the issue on your own and can share your findings with everyone. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000000..6a4fb83151d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,46 @@ +name: Publishing + +on: + release: + types: [published] + +permissions: + id-token: write # Required for OIDC (Trusted Publishing) + contents: read + +jobs: + npm-publish: + if: ${{ !github.event.release.draft }} + runs-on: ubuntu-latest + environment: npmjs + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.target_commitish }} + - run: git fetch --force --tags + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - run: npm run publish-please-only + docker-publish: + needs: npm-publish + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.target_commitish }} + - run: git fetch --force --tags + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: 'https://registry.npmjs.org' + - uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - run: npm ci + - run: npx gulp build + - run: gulp docker-publish \ No newline at end of file diff --git a/.github/workflows/release-commenter.yml b/.github/workflows/release-commenter.yml new file mode 100644 index 00000000000..0da3a7427c1 --- /dev/null +++ b/.github/workflows/release-commenter.yml @@ -0,0 +1,14 @@ +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: apexskier/github-release-commenter@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + comment-template: | + Release {release_link} addresses this. diff --git a/.github/workflows/release-lock.yml b/.github/workflows/release-lock.yml new file mode 100644 index 00000000000..707a521e097 --- /dev/null +++ b/.github/workflows/release-lock.yml @@ -0,0 +1,17 @@ +# This is a basic workflow to help you get started with Actions + +name: Release lock + +# Controls when the workflow will run +on: + pull_request_target: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + lock: + runs-on: ubuntu-latest + + steps: + - uses: DevExpress/testcafe-build-system/actions/release-lock@main + with: + lock: ${{ secrets.RELEASE_LOCK }} diff --git a/.github/workflows/request-deploy.yml b/.github/workflows/request-deploy.yml new file mode 100644 index 00000000000..c56f4c76a9d --- /dev/null +++ b/.github/workflows/request-deploy.yml @@ -0,0 +1,87 @@ +name: Request Deployment + +on: + push: + branches: [ master ] + pull_request_target: + branches: [ master ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + authenticate: + runs-on: ubuntu-latest + environment: authentication + if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.author_association == 'NONE' }} + + steps: + - name: Check permissions + uses: actions/github-script@v6 + with: + script: | + console.log("${{github.event_name}}", await github.rest.repos.listCommitStatusesForRef({ + repo: context.repo.repo, + owner: context.repo.owner, + ref: '9e1ca0defd42f7b3de88188f4d32d8c172c86242' + })) + + request-deploy: + runs-on: ubuntu-latest + environment: CI + needs: authenticate + if: ${{ !cancelled() && (needs.authenticate.result == 'success' || needs.authenticate.result == 'skipped') }} + + steps: + - name: Request Deployment + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + + async function getPullRequestHeadCommit (pr) { + if (!pr) + throw new Error('Failed to retrieve the PR information'); + + for (let i = 0; i < 30 && pr.mergeable === null; i++) { + console.log('Waiting for the merge commit...'); + + await delay(3000); + + ( + { data: pr } = await github.rest.pulls.get({ + owner: pr.base.repo.owner.login, + repo: pr.base.repo.name, + pull_number: pr.number + }) + ); + } + + if (!pr.mergeable) + throw new Error('PR cannot be merged'); + + const sha = pr.head.sha; + + return { sha, merged_sha: pr.merge_commit_sha, base_sha: pr.base.sha }; + } + + async function getTargetCommit(context) { + if (context.eventName === 'push' && context.payload.head_commit) + return { sha: context.payload.after, base_sha: context.payload.before }; + + if (context.eventName === 'pull_request_target') + return getPullRequestHeadCommit(context.payload.pull_request); + + throw new Error('Failed to detect a target commit'); + } + + const inputs = await getTargetCommit(context); + + await github.rest.actions.createWorkflowDispatch({ + ref: 'master', + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'deploy-to-artifacts.yml', + inputs + }); + diff --git a/.github/workflows/test-client-desktop.yml b/.github/workflows/test-client-desktop.yml new file mode 100644 index 00000000000..704db5fcdb4 --- /dev/null +++ b/.github/workflows/test-client-desktop.yml @@ -0,0 +1,20 @@ +name: Test Client (Desktop) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-client.yml + with: + test-script: 'npx gulp test-client-travis-run --steps-as-tasks' + browsers: '["microsoftedge", "chrome", "firefox", "safari"]' + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/test-client-mobile.yml b/.github/workflows/test-client-mobile.yml new file mode 100644 index 00000000000..a1698fcd34b --- /dev/null +++ b/.github/workflows/test-client-mobile.yml @@ -0,0 +1,20 @@ +name: Test Client (Mobile) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-client.yml + with: + test-script: 'npx gulp test-client-travis-mobile-run --steps-as-tasks' + browsers: '["chrome", "safari"]' + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/test-client.yml b/.github/workflows/test-client.yml new file mode 100644 index 00000000000..31a35914c7e --- /dev/null +++ b/.github/workflows/test-client.yml @@ -0,0 +1,76 @@ +name: Test Client + +on: + workflow_call: + inputs: + test-script: + required: true + type: string + browsers: + required: true + type: string +jobs: + test: + runs-on: ubuntu-latest + name: ${{ matrix.browser }} + continue-on-error: true + strategy: + matrix: + browser: ${{ fromJSON(inputs.browsers) }} + environment: test-client + env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + CLIENT_TESTS_CURRENT_BROWSER: ${{ matrix.browser }} + steps: + - uses: DevExpress/testcafe-build-system/actions/set-status@main + with: + status: 'pending' + + - uses: actions/checkout@v3 + with: + ref: ${{github.event.inputs.merged_sha || github.event.inputs.sha}} + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - uses: DevExpress/testcafe-build-system/actions/read-artifacts@main + with: + package-name: 'testcafe' + + - name: Get npm cache directory + id: npm-cache-dir + run: | + echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npm ci + + - run: ${{ inputs.test-script }} + timeout-minutes: 60 + + - uses: DevExpress/testcafe-build-system/actions/save-matrix-status@main + if: always() + with: + job-id: ${{ matrix.browser }} + + set-result-status: + if: always() + needs: test + runs-on: ubuntu-latest + steps: + - uses: DevExpress/testcafe-build-system/actions/read-matrix-status@main + id: matrix-status + + - uses: DevExpress/testcafe-build-system/actions/set-status@main + with: + status: ${{ steps.matrix-status.outputs.status }} + + - name: Exit with error + if: ${{ steps.matrix-status.outputs.status != 'success' }} + run: exit 1 \ No newline at end of file diff --git a/.github/workflows/test-dependencies.yml b/.github/workflows/test-dependencies.yml new file mode 100644 index 00000000000..4a7f9dbf236 --- /dev/null +++ b/.github/workflows/test-dependencies.yml @@ -0,0 +1,19 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Test Dependencies + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + audit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - run: npm i --package-lock-only + - run: npm audit --production diff --git a/.github/workflows/test-functional-docker.yml b/.github/workflows/test-functional-docker.yml new file mode 100644 index 00000000000..d8abac943d8 --- /dev/null +++ b/.github/workflows/test-functional-docker.yml @@ -0,0 +1,20 @@ +name: Test Functional (Docker) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp docker-functional-test-run --steps-as-tasks' + retry_failed_tests: false + is-docker: true diff --git a/.github/workflows/test-functional-local-chrome.yml b/.github/workflows/test-functional-local-chrome.yml new file mode 100644 index 00000000000..4de9ed11936 --- /dev/null +++ b/.github/workflows/test-functional-local-chrome.yml @@ -0,0 +1,18 @@ +name: Test Functional (Local Chrome) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-headless-chrome-run --steps-as-tasks' \ No newline at end of file diff --git a/.github/workflows/test-functional-local-edge.yml b/.github/workflows/test-functional-local-edge.yml new file mode 100644 index 00000000000..23edb794182 --- /dev/null +++ b/.github/workflows/test-functional-local-edge.yml @@ -0,0 +1,20 @@ +name: Test Functional (Local Edge) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + matrix-jobs-count: 5 + timeout: 45 + test-script: 'npx gulp test-functional-local-headless-edge-run --steps-as-tasks' diff --git a/.github/workflows/test-functional-local-esm.yml b/.github/workflows/test-functional-local-esm.yml new file mode 100644 index 00000000000..109a338c5fa --- /dev/null +++ b/.github/workflows/test-functional-local-esm.yml @@ -0,0 +1,18 @@ +name: Test Functional (ESM) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp prepare-functional-tests --steps-as-tasks && npm run test-functional-local-headless-chrome-run-esm' \ No newline at end of file diff --git a/.github/workflows/test-functional-local-firefox.yml b/.github/workflows/test-functional-local-firefox.yml new file mode 100644 index 00000000000..ec263b887ab --- /dev/null +++ b/.github/workflows/test-functional-local-firefox.yml @@ -0,0 +1,18 @@ +name: Test Functional (Local Firefox) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-headless-firefox-run --steps-as-tasks' \ No newline at end of file diff --git a/.github/workflows/test-functional-local-headed-browsers.yml b/.github/workflows/test-functional-local-headed-browsers.yml new file mode 100644 index 00000000000..f420b57e52f --- /dev/null +++ b/.github/workflows/test-functional-local-headed-browsers.yml @@ -0,0 +1,19 @@ +name: Test Functional (Headed Browsers) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-chrome-firefox-headed-run --steps-as-tasks' + display: ':99.0' diff --git a/.github/workflows/test-functional-local-legacy.yml b/.github/workflows/test-functional-local-legacy.yml new file mode 100644 index 00000000000..6b2f09845d0 --- /dev/null +++ b/.github/workflows/test-functional-local-legacy.yml @@ -0,0 +1,18 @@ +name: Test Functional (Local Legacy) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-legacy-run --steps-as-tasks' \ No newline at end of file diff --git a/.github/workflows/test-functional-local-multiple-windows-na.yml b/.github/workflows/test-functional-local-multiple-windows-na.yml new file mode 100644 index 00000000000..4820675cb09 --- /dev/null +++ b/.github/workflows/test-functional-local-multiple-windows-na.yml @@ -0,0 +1,19 @@ +name: Test Functional (Multiple Windows in Native Automation mode) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-multiple-windows-na-run --steps-as-tasks' + display: ':99.0' diff --git a/.github/workflows/test-functional-local-multiple-windows.yml b/.github/workflows/test-functional-local-multiple-windows.yml new file mode 100644 index 00000000000..34eea353fc7 --- /dev/null +++ b/.github/workflows/test-functional-local-multiple-windows.yml @@ -0,0 +1,19 @@ +name: Test Functional (Multiple Windows) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-multiple-windows-run --steps-as-tasks' + display: ':99.0' diff --git a/.github/workflows/test-functional-local-native-automation.yml b/.github/workflows/test-functional-local-native-automation.yml new file mode 100644 index 00000000000..3546f4f7a0d --- /dev/null +++ b/.github/workflows/test-functional-local-native-automation.yml @@ -0,0 +1,19 @@ +name: Test Functional (Local Chrome Native Automation) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-native-automation-run --steps-as-tasks' + display: ':99.0' diff --git a/.github/workflows/test-functional-local-safari.yml b/.github/workflows/test-functional-local-safari.yml new file mode 100644 index 00000000000..ec3e255f76f --- /dev/null +++ b/.github/workflows/test-functional-local-safari.yml @@ -0,0 +1,22 @@ +name: Test Functional (Local Safari) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-local-safari-run --steps-as-tasks' + os: 'macos-13' + timeout: 40 + matrix-jobs-count: 5 + node-version: 20 diff --git a/.github/workflows/test-functional-remote-mobile.yml b/.github/workflows/test-functional-remote-mobile.yml new file mode 100644 index 00000000000..b6d471d9edb --- /dev/null +++ b/.github/workflows/test-functional-remote-mobile.yml @@ -0,0 +1,23 @@ +name: Test Functional (Remote Mobile) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-functional.yml + with: + test-script: 'npx gulp test-functional-travis-mobile-run --steps-as-tasks' + os: 'ubuntu-latest' + use-public-hostname: true + timeout: 35 + is-browserstack: true + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/test-functional.yml b/.github/workflows/test-functional.yml new file mode 100644 index 00000000000..bc51bfaa80f --- /dev/null +++ b/.github/workflows/test-functional.yml @@ -0,0 +1,187 @@ +name: Test Functional + +on: + workflow_call: + inputs: + test-script: + required: true + type: string + os: + required: false + type: string + default: ubuntu-latest + node-version: + required: false + type: number + default: 16 + matrix-jobs-count: + required: false + type: number + default: 3 + display: + required: false + type: string + default: '' + use-public-hostname: + required: false + type: boolean + default: false + retry_failed_tests: + required: false + type: boolean + default: true + timeout: + required: false + type: number + default: 25 + is-docker: + required: false + type: boolean + default: false + is-browserstack: + required: false + type: boolean + default: false +env: + NO_CACHE: ${{ secrets.NO_CACHE }} + +jobs: + prepare-matrix: + runs-on: ubuntu-latest + outputs: + matrix: "${{ steps.generate-matrix.outputs.result }}" + steps: + - uses: actions/github-script@v6 + id: generate-matrix + with: + script: | + return Array.from({length: ${{ inputs.matrix-jobs-count }}}, (_, i) => i + 1) + + test: + needs: prepare-matrix + runs-on: ${{ inputs.os }} + continue-on-error: true + strategy: + matrix: + test-group: ${{ fromJSON(needs.prepare-matrix.outputs.matrix) }} + environment: test-functional + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + BROWSERSTACK_BUILD_NAME: ${{ format('{0} ({1})', github.workflow, github.run_id) }} + USE_PUBLIC_HOSTNAME: ${{ inputs.use-public-hostname }} + RETRY_FAILED_TESTS: ${{ inputs.retry_failed_tests }} + TEST_GROUPS_COUNT: ${{ inputs.matrix-jobs-count }} + TEST_GROUP_NUMBER: ${{ matrix.test-group }} + DEBUG: 'testcafe*' + DISPLAY: ${{ inputs.display }} + steps: + - name: Set 'pending' status + uses: DevExpress/testcafe-build-system/actions/set-status@main + with: + status: 'pending' + + - run: sudo safaridriver --enable + if: ${{ contains(inputs.os, 'macos') }} + + - run: | + sudo apt install fluxbox + Xvfb ${{ inputs.display }} -screen 0 1920x1080x24 & + sleep 3 + fluxbox >/dev/null 2>&1 & + if: ${{ inputs.display }} + + - uses: actions/checkout@v3 + with: + ref: ${{github.event.inputs.merged_sha || github.event.inputs.sha}} + + - uses: actions/setup-node@v3 + with: + node-version: ${{ inputs.node-version }} + + # Remove after https://github.com/actions/runner-images/issues/8114 will be fixed + - name: Remove Google Chrome for Testing + if: ${{ contains(inputs.os, 'windows') }} + run: Remove-Item -Path "C:\Program Files\Google\Chrome" -Force -Recurse + + - name: Install Google Chrome + if: ${{ contains(inputs.os, 'windows') }} + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + + - uses: DevExpress/testcafe-build-system/actions/read-artifacts@main + with: + package-name: 'testcafe' + is-docker: ${{ inputs.is-docker }} + + - name: Get npm cache directory + id: npm-cache-dir + shell: bash + run: | + echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true' + if: ${{ !env.NO_CACHE }} + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npm ci + if: ${{ !inputs.is-docker }} + - run: npm install + if: ${{ inputs.is-docker }} + + - name: Add permissions on MacOS + run: | + sudo sqlite3 "$HOME/Library/Application Support/com.apple.TCC/TCC.db" "INSERT OR REPLACE INTO access VALUES('kTCCServiceAppleEvents','com.devexpress.testcafe-browser-tools',0,2,3,1,X'fade0c0000000068000000010000000700000007000000080000001443fa4ca5141baeda21aeca1f50894673b440d4690000000800000014f8afcf6e69791b283e55bd0b03e39e422745770e0000000800000014bf4fc1aed64c871a49fc6bc9dd3878ce5d4d17c6',NULL,0,'com.apple.Safari',X'fade0c000000002c00000001000000060000000200000010636f6d2e6170706c652e53616661726900000003',NULL,1687952810);" + sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "INSERT OR REPLACE INTO access VALUES('kTCCServiceScreenCapture','com.devexpress.testcafe-browser-tools',0,2,3,1,X'fade0c0000000068000000010000000700000007000000080000001443fa4ca5141baeda21aeca1f50894673b440d4690000000800000014f8afcf6e69791b283e55bd0b03e39e422745770e0000000800000014bf4fc1aed64c871a49fc6bc9dd3878ce5d4d17c6',NULL,0,'UNUSED',NULL,0,1687952810);" + if: ${{ contains(inputs.os, 'mac') }} + + - name: 'Start BrowserStackLocal Tunnel' + if: ${{ inputs.is-browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: start + local-logging-level: false + local-identifier: random + + - run: ${{ inputs.test-script }} 2> testcafe-debug-log-${{ matrix.test-group }}.log + timeout-minutes: ${{ inputs.timeout }} + + - name: 'Stop BrowserStackLocal' + if: ${{ inputs.is-browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: stop + + - name: Save debug log + if: always() + uses: actions/upload-artifact@v4 + with: + name: testcafe-debug-log-${{ matrix.test-group }} + path: testcafe-debug-log-${{ matrix.test-group }}.log + + - uses: DevExpress/testcafe-build-system/actions/save-matrix-status@main + if: always() + with: + job-id: ${{ matrix.test-group }} + + set-result-status: + if: always() + needs: test + runs-on: ubuntu-latest + steps: + - uses: DevExpress/testcafe-build-system/actions/read-matrix-status@main + id: matrix-status + + - uses: DevExpress/testcafe-build-system/actions/set-status@main + if: always() + with: + status: ${{ steps.matrix-status.outputs.status }} + + - name: Exit with error + if: ${{ steps.matrix-status.outputs.status != 'success' }} + run: exit 1 \ No newline at end of file diff --git a/.github/workflows/test-server-docker.yml b/.github/workflows/test-server-docker.yml new file mode 100644 index 00000000000..52264cd4c3a --- /dev/null +++ b/.github/workflows/test-server-docker.yml @@ -0,0 +1,19 @@ +name: Test Server (Docker) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-server.yml + with: + test-script: 'npx gulp docker-server-test-run --steps-as-tasks' + is-docker: true diff --git a/.github/workflows/test-server-latest.yml b/.github/workflows/test-server-latest.yml new file mode 100644 index 00000000000..be749af64de --- /dev/null +++ b/.github/workflows/test-server-latest.yml @@ -0,0 +1,21 @@ +name: Test Server (Latest) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-server.yml + with: + test-script: 'npx gulp test-server-run --steps-as-tasks' + node-version: 'latest' + node-options: '--no-experimental-strip-types' + secrets: inherit diff --git a/.github/workflows/test-server-minimal.yml b/.github/workflows/test-server-minimal.yml new file mode 100644 index 00000000000..c365f761d3e --- /dev/null +++ b/.github/workflows/test-server-minimal.yml @@ -0,0 +1,19 @@ +name: Test Server (Minimum) + +on: + workflow_dispatch: + inputs: + sha: + description: 'The test commit SHA or ref' + required: true + default: 'master' + merged_sha: + description: 'The merge commit SHA' + deploy_run_id: + description: 'The ID of a deployment workspace run with artifacts' +jobs: + test: + uses: ./.github/workflows/test-server.yml + with: + test-script: 'npx gulp test-server-run --steps-as-tasks' + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/test-server.yml b/.github/workflows/test-server.yml new file mode 100644 index 00000000000..adf551e5623 --- /dev/null +++ b/.github/workflows/test-server.yml @@ -0,0 +1,70 @@ +name: Test Server + +on: + workflow_call: + inputs: + test-script: + required: true + type: string + node-version: + required: false + type: string + default: 16 + is-docker: + required: false + type: boolean + default: false + node-options: + required: false + type: string + default: '' +env: + NO_CACHE: ${{ secrets.NO_CACHE }} +jobs: + test: + runs-on: ubuntu-latest + environment: test-server + steps: + - uses: DevExpress/testcafe-build-system/actions/set-status@main + with: + status: 'pending' + + - uses: actions/checkout@v3 + with: + ref: ${{github.event.inputs.merged_sha || github.event.inputs.sha}} + + - uses: actions/setup-node@v3 + with: + node-version: ${{ inputs.node-version }} + + - uses: DevExpress/testcafe-build-system/actions/read-artifacts@main + with: + package-name: 'testcafe' + is-docker: ${{ inputs.is-docker }} + + - name: Get npm cache directory + id: npm-cache-dir + run: | + echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + if: ${{ !env.NO_CACHE }} + id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true' + with: + path: ${{ steps.npm-cache-dir.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npm ci + if: ${{ !inputs.is-docker }} + - run: npm install + if: ${{ inputs.is-docker }} + + - run: ${{ inputs.test-script }} + timeout-minutes: 60 + env: + NODE_OPTIONS: ${{ inputs.node-options }} + + - uses: DevExpress/testcafe-build-system/actions/set-status@main + if: always() + with: + status: ${{ fromJSON('["failure", "success"]')[job.status == 'success'] }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 280e52f4860..f33f4f631bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,17 @@ -node_modules +**/node_modules/* +!**/node_modules/test-module/ +/ts-defs .idea +.vscode /lib /site .sass-cache .publish /___test-screenshots___ -yarn.lock +___test-videos___ +/screenshots +Gemfile.lock +.npmrc +.DS_Store +!/gulp +/test/functional/fixtures/**/package.json diff --git a/.md-lint/blog.json b/.md-lint/blog.json new file mode 100644 index 00000000000..d9a2750ed5a --- /dev/null +++ b/.md-lint/blog.json @@ -0,0 +1,10 @@ +{ + "MD009": { "br_spaces": 2 }, + "MD013": false, + "MD007": false, + "MD036": false, + "MD024": false, + "MD025": false, + "MD029": {"style": "ordered"}, + "MD047": false +} diff --git a/.md-lint/changelog.json b/.md-lint/changelog.json index 850326081c9..a3a7bc8e672 100644 --- a/.md-lint/changelog.json +++ b/.md-lint/changelog.json @@ -4,5 +4,6 @@ "MD007": false, "MD024": false, "MD036": false, - "MD029": {"style": "ordered"} + "MD029": {"style": "ordered"}, + "MD047": false } diff --git a/.md-lint/docs.json b/.md-lint/docs.json index 565aeee68c7..7911e6f3c07 100644 --- a/.md-lint/docs.json +++ b/.md-lint/docs.json @@ -2,6 +2,9 @@ "MD009": { "br_spaces": 2 }, "MD013": false, "MD007": false, + "MD025": false, + "MD029": {"style": "ordered"}, "MD036": false, - "MD029": {"style": "ordered"} -} \ No newline at end of file + "MD047": false, + "MD026": false +} diff --git a/.md-lint/faq.json b/.md-lint/faq.json new file mode 100644 index 00000000000..72f40799fbe --- /dev/null +++ b/.md-lint/faq.json @@ -0,0 +1,10 @@ +{ + "MD009": { "br_spaces": 2 }, + "MD013": false, + "MD007": false, + "MD036": false, + "MD029": {"style": "ordered"}, + "MD025": false, + "MD026": false, + "MD047": false +} diff --git a/.md-lint/readme.json b/.md-lint/readme.json index 176647adf0d..9a2ff2ae961 100644 --- a/.md-lint/readme.json +++ b/.md-lint/readme.json @@ -6,5 +6,6 @@ "MD036": false, "MD033": false, "MD041": false, - "MD029": {"style": "ordered"} + "MD029": {"style": "ordered"}, + "MD047": false } diff --git a/.md-lint/recipes.json b/.md-lint/recipes.json index 7f4436a657c..c5f98efd847 100644 --- a/.md-lint/recipes.json +++ b/.md-lint/recipes.json @@ -2,7 +2,9 @@ "MD009": { "br_spaces": 2 }, "MD013": false, "MD007": false, - "MD036": false, + "MD025": false, "MD029": {"style": "ordered"}, - "MD033": false -} \ No newline at end of file + "MD033": false, + "MD036": false, + "MD047": false +} diff --git a/.md-lint/templates.json b/.md-lint/templates.json new file mode 100644 index 00000000000..4c7db9e7803 --- /dev/null +++ b/.md-lint/templates.json @@ -0,0 +1,10 @@ +{ + "MD009": { "br_spaces": 2 }, + "MD013": false, + "MD007": false, + "MD025": false, + "MD029": {"style": "ordered"}, + "MD036": false, + "MD047": false, + "MD041": false +} \ No newline at end of file diff --git a/.publishrc b/.publishrc index 16aa87eb911..5b2e51946d1 100644 --- a/.publishrc +++ b/.publishrc @@ -7,8 +7,8 @@ "branch": "master", "gitTag": true }, - "confirm": true, - "publishTag": "alpha", + "confirm": false, + "publishTag": "latest", "prePublishScript": "gulp test-server", - "postPublishScript": "gulp docker-publish" + "postPublishScript": "" } diff --git a/.travis-docs.yml b/.travis-docs.yml deleted file mode 100644 index 6d1f4027626..00000000000 --- a/.travis-docs.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: node_js -matrix: - include: - - node_js: "7" - env: GULP_TASK="test-docs-travis" JEKYLL_ENV="testing" - fast_finish: true - -before_install: - - rvm install 2.1.5 - - curl -o- -L https://yarnpkg.com/install.sh | bash - - export PATH=$HOME/.yarn/bin:$PATH - - yarn install - -cache: yarn - -install: gem install jekyll htmlentities sanitize redcarpet jekyll-sitemap - -branches: - except: - - /^build-bot-temp-.*$/ - -notifications: - email: false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d4faba300cf..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: node_js -matrix: - include: - - node_js: "4" - env: GULP_TASK="test-server" - - node_js: "stable" - env: GULP_TASK="test-server" - - node_js: "stable" - env: GULP_TASK="test-client-travis" - - node_js: "stable" - env: GULP_TASK="test-client-travis-mobile" - # We use Node 7 here as a temporary workaround. Browsers on OS X sometimes can't open a tested - # page under Node 8 on BrowserStack for some reason. As a result we have blinking tests. - - node_js: "7" - env: GULP_TASK="test-functional-travis-desktop-osx-and-ms-edge" - - node_js: "7" - env: GULP_TASK="test-functional-travis-mobile" - fast_finish: true - -cache: yarn - -before_install: - - curl -o- -L https://yarnpkg.com/install.sh | bash - - export PATH=$HOME/.yarn/bin:$PATH - -install: yarn - -branches: - except: - - /^build-bot-temp-.*$/ - -notifications: - email: false diff --git a/@types/chrome-remote-interface/index.d.ts b/@types/chrome-remote-interface/index.d.ts new file mode 100644 index 00000000000..88084dbd58f --- /dev/null +++ b/@types/chrome-remote-interface/index.d.ts @@ -0,0 +1,33 @@ +declare module 'chrome-remote-interface' { + namespace chromeRemoteInterface { + export type ProtocolApi = import('devtools-protocol/types/protocol-proxy-api').ProtocolProxyApi.ProtocolApi; + + export type ProtocolTargetInfo = import('devtools-protocol/types/protocol').Protocol.Target.TargetInfo; + + export interface TargetInfo extends ProtocolTargetInfo { + id: string; + } + + export interface GenericConnectionOptions { + port: number; + } + + export interface ConstructorOptions extends GenericConnectionOptions { + target: TargetInfo; + } + + export interface CloseTabOptions extends GenericConnectionOptions { + id: TargetInfo['id']; + } + } + + interface ChromeRemoteInterface { + (options: chromeRemoteInterface.ConstructorOptions): Promise; + List (options: chromeRemoteInterface.GenericConnectionOptions): Promise; + Close (options: chromeRemoteInterface.CloseTabOptions): Promise; + } + + const chromeRemoteInterface: ChromeRemoteInterface; + + export = chromeRemoteInterface; +} diff --git a/@types/error-stack-parser/index.d.ts b/@types/error-stack-parser/index.d.ts new file mode 100644 index 00000000000..18c6066b271 --- /dev/null +++ b/@types/error-stack-parser/index.d.ts @@ -0,0 +1,5 @@ +declare module 'error-stack-parser' { + export interface StackFrame { + getFileName(): string; + } +} diff --git a/@types/humanize-duration/index.d.ts b/@types/humanize-duration/index.d.ts new file mode 100644 index 00000000000..ffaf7b321b6 --- /dev/null +++ b/@types/humanize-duration/index.d.ts @@ -0,0 +1,5 @@ +declare module 'humanize-duration' { + function HumanizeDuration(ms: number): string; + + export = HumanizeDuration; +} diff --git a/@types/indent-string/index.d.ts b/@types/indent-string/index.d.ts new file mode 100644 index 00000000000..e5f2f7a629c --- /dev/null +++ b/@types/indent-string/index.d.ts @@ -0,0 +1,3 @@ +declare module 'indent-string' { + export default function (str: string, indent: string, count: number): string; +} diff --git a/@types/is-ci/index.d.ts b/@types/is-ci/index.d.ts new file mode 100644 index 00000000000..76108d8acbb --- /dev/null +++ b/@types/is-ci/index.d.ts @@ -0,0 +1,5 @@ +declare module 'is-ci' { + const isCI: boolean; + + export default isCI; +} diff --git a/@types/os-family/index.d.ts b/@types/os-family/index.d.ts new file mode 100644 index 00000000000..fe37a777fe1 --- /dev/null +++ b/@types/os-family/index.d.ts @@ -0,0 +1,9 @@ +declare module 'os-family' { + namespace OSFamily { + const win: boolean; + const linux: boolean; + const mac: boolean; + } + + export = OSFamily; +} diff --git a/@types/pinkie/index.d.ts b/@types/pinkie/index.d.ts new file mode 100644 index 00000000000..33f66530dc8 --- /dev/null +++ b/@types/pinkie/index.d.ts @@ -0,0 +1,17 @@ +declare module 'pinkie' { + interface Promise { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; + } + + interface PromiseConstructor { + new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; + reject(reason?: any): Promise; + resolve(value: T | PromiseLike): Promise; + resolve(): Promise; + } + + const Promise: PromiseConstructor; + + export default Promise; +} diff --git a/@types/pretty-hr-time/index.d.ts b/@types/pretty-hr-time/index.d.ts new file mode 100644 index 00000000000..925b1e909a8 --- /dev/null +++ b/@types/pretty-hr-time/index.d.ts @@ -0,0 +1,12 @@ +declare module 'pretty-hrtime' { + namespace prettyHrtime { + interface Options { + verbose?: boolean; + precise?: boolean; + } + } + + function prettyHrtime(hrTime: [number, number], options?: prettyHrtime.Options): string; + + export = prettyHrtime; +} diff --git a/@types/promisify-event/index.d.ts b/@types/promisify-event/index.d.ts new file mode 100644 index 00000000000..107d2095c5b --- /dev/null +++ b/@types/promisify-event/index.d.ts @@ -0,0 +1,5 @@ +declare module 'promisify-event' { + import EventEmitter = NodeJS.EventEmitter; + + export default function (emitter: EventEmitter, event: string): Promise; +} diff --git a/@types/qrcode-terminal/index.d.ts b/@types/qrcode-terminal/index.d.ts new file mode 100644 index 00000000000..cf881199f8e --- /dev/null +++ b/@types/qrcode-terminal/index.d.ts @@ -0,0 +1,3 @@ +declare module 'qrcode-terminal' { + export function generate (input: string, callback?: Function): void; +} diff --git a/@types/read-file-relative/index.d.ts b/@types/read-file-relative/index.d.ts new file mode 100644 index 00000000000..68f26bb64a4 --- /dev/null +++ b/@types/read-file-relative/index.d.ts @@ -0,0 +1,4 @@ +declare module 'read-file-relative' { + export function readSync(relativePath: string, binary?: boolean): Buffer | string; + export function read(relativePath: string, options:{ [key: string]: any }, callback?: Function): void; +} diff --git a/@types/replicator/index.d.ts b/@types/replicator/index.d.ts new file mode 100644 index 00000000000..b348c4f3385 --- /dev/null +++ b/@types/replicator/index.d.ts @@ -0,0 +1,24 @@ +declare module 'replicator' { + export interface Transform { + type: string; + shouldTransform (type: string, val: unknown): boolean; + toSerializable (val: unknown): unknown; + fromSerializable (val: unknown): unknown; + } + + interface Replicator { + removeTransforms(transforms: Transform | Transform[]): Replicator; + addTransforms(transforms: Transform | Transform[]): Replicator; + transforms: Transform[]; + decode (val: unknown): unknown; + encode (val: unknown): string; + } + + interface ReplicatorConstructor { + new (serializer?: { serialize: (val: unknown) => unknown, deserialize: (val: unknown) => unknown }): Replicator; + } + + const Replicator: ReplicatorConstructor; + + export default Replicator; +} diff --git a/@types/testcafe-browser-tools/index.d.ts b/@types/testcafe-browser-tools/index.d.ts new file mode 100644 index 00000000000..548173498ff --- /dev/null +++ b/@types/testcafe-browser-tools/index.d.ts @@ -0,0 +1,19 @@ +declare module 'testcafe-browser-tools/lib/errors' { + export class UnableToAccessScreenRecordingAPIError extends Error { + + } +} + +declare module 'testcafe-browser-tools' { + export function close(windowDescriptor: string | any): Promise; + export function findWindow(pageTitle: string): any; + export function generateThumbnail(sourcePath: string, thumbnailPath: string, width: number, height: number): Promise; + export function isMaximized(windowDescriptor?: string | any): Promise; + export function resize(windowDescriptor: string | any, currentWidth: number, currentHeight: number, width: number, height: number): Promise; + export function maximize(windowDescriptor: string | any): Promise; + export function screenshot(windowDescriptor: string | any, screenshotPath: string): Promise; + + import * as errors from 'testcafe-browser-tools/lib/errors'; + + export { errors }; +} diff --git a/@types/time-limit-promise/index.d.ts b/@types/time-limit-promise/index.d.ts new file mode 100644 index 00000000000..8d29946a624 --- /dev/null +++ b/@types/time-limit-promise/index.d.ts @@ -0,0 +1,3 @@ +declare module 'time-limit-promise' { + export default function (promise: Promise, timeout: number, options?: { resolveWith: any } | { rejectWith: any }): Promise; +} diff --git a/@types/unquote/index.d.ts b/@types/unquote/index.d.ts new file mode 100644 index 00000000000..23fe85b50f1 --- /dev/null +++ b/@types/unquote/index.d.ts @@ -0,0 +1,3 @@ +declare module 'unquote' { + export default function (value: string): string; +} diff --git a/CHANGELOG.md b/CHANGELOG.md index d92bed1e127..e32f5400295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,3980 @@ # Changelog +## v3.7.2 (2025-02-18) + +### Bug Fixes + +* TestCafe incorrectly processes angular functions in proxy mode. ([3035](https://github.com/DevExpress/testcafe-hammerhead/issues/3035)) +* A browser throws a SyntaxError in proxy mode. ([8368](https://github.com/DevExpress/testcafe/issues/8368)) +* Docker Image cannot run test for pages in foreign languages. ([8362](https://github.com/DevExpress/testcafe/issues/8362)) +* [Native Automation] TestCafe does not maximize the window after a resizeMethod call. ([8360](https://github.com/DevExpress/testcafe/issues/8360)) +* lru-cache conflicts with TypeScript's latest types. ([3036](https://github.com/DevExpress/testcafe-hammerhead/issues/3036)) + +## v3.7.1 (2024-12-18) + +### Bug Fixes + +* The 'click' event is raised when a draggable element was dropped. ([8250](https://github.com/DevExpress/testcafe/issues/8250)) +* Unable to type text in the CodeMirror editor after it was clicked. ([8321](https://github.com/DevExpress/testcafe/issues/8321)) +* Angular functions can work incorrectly in proxy mode. ([8221](https://github.com/DevExpress/testcafe/issues/8221)) + +## TestCafe v3.7.0 Released + +The TestCafe v3.7.0 update includes the capability to use `Metadata` as an interface, `esm` configuration file option, and a number of bug fixes. + +meta-readmore + +### Declare Metadata Interface + +Earlier versions of TestCafe supported `Metadata` as a type. In TestCafe v3.7.0 and higher, you should declare `Metadata` as an interface. + +```js +// testcafe.global.d.ts +declare module "testcafe" { + global { + interface Metadata { + manual?: boolean, + } + } +} +``` + +### New Configuration File Option: esm + +Earlier versions of TestCafe supported the [ESM Module](https://testcafe.io/documentation/404258/guides/advanced-guides/esm-module-support) from the CLI only. In TestCafe v3.7.0 and higher, you can use the [esm](https://testcafe.io/documentation/402638/reference/configuration-file#esm) configuration file option. Note that this option **only works** with Node.js 18.19-18.xx, and 20.8.0 and up. + +### Bug Fixes + +* CDP client tab creation causes a `WebSocket connection closed` error in Chrome v130 ([#8286](https://github.com/DevExpress/testcafe/issues/8286)). +* An unhandled promise rejection occurs while launching sub-windows ([#8258](https://github.com/DevExpress/testcafe/issues/8258)). +* Assertions that perform visibility checks fail despite elements being visible on the page ([#8237](https://github.com/DevExpress/testcafe/issues/8237)). +* The [t.getCurrentCDPSession](https://testcafe.io/documentation/404913/reference/test-api/testcontroller/getcurrentcdpsession) method returns `unknown` ([#8228](https://github.com/DevExpress/testcafe/issues/8228)). +* [Native Automation] Request hooks are applied to skipped tests ([#8229](https://github.com/DevExpress/testcafe/issues/8229)). +* A `WebSocket connection closed` error occurs while running TestCafe in Chrome v127. The updated version includes the `--disable-search-engine-choice-screen` flag ([#8240](https://github.com/DevExpress/testcafe/issues/8240)). +* A `leaveElement` method call causes an error when the `prevElement` object is removed from the DOM ([#8264](https://github.com/DevExpress/testcafe/issues/8264)). + +## v3.6.2 (2024-07-02) + +### Bug Fixes + +* [Native Automation] TestCafe incorrectly uploads files in Chrome v125 and up. ([#8198](https://github.com/DevExpress/testcafe/issues/8198)) +* TestCafe dependencies include the vulnerable endpoint-utils package ([#8207](https://github.com/DevExpress/testcafe/issues/8207)). The updated version includes the address package instead. + +## v3.6.1 (2024-06-10) + +### Bug Fixes + +* TestCafe incorrectly calculates the dimensions of multi-line elements. ([#8179](https://github.com/DevExpress/testcafe/issues/8179)) +* TestCafe incorrectly executes the `resizeWindow` method if you maximize the target window first. ([#8157](https://github.com/DevExpress/testcafe/issues/8157)) + +## TestCafe v3.6.0 Released + +The TestCafe v3.6.0 update includes two minor changes and a number of bug fixes. + +## New method: t.getCurrentCDPSession + +The [t.getCurrentCDPSession](xref:404913) method allows native automation users to examine and control the CDP connection between TestCafe and the browser. + +Use the method to obtain the Chrome DevTools Protocol object for the current session. The CDP object exposes [properties and methods](https://chromedevtools.github.io/devtools-protocol/) that pertain to the CDP connection between TestCafe and the active browser window. + +```js +fixture `Get current CDP session` + .page('https://devexpress.github.io/testcafe/example'); + +test(`Get current CDP session`, async t => { + const mainWindowId = await t.testRun.activeWindowId; + + let clientCDP = await t.getCurrentCDPSession(); + + await t.expect(clientCDP.webSocketUrl).contains(mainWindowId); +} +``` + +## Headless browser connection + +TestCafe v3.6.0 takes advantage of the recent [Chromium headless mode upgrade](https://developer.chrome.com/docs/chromium/new-headless). The new headless mode offers better reliability and higher emulation accuracy. + +The headless mode upgrade may cause unexpected changes to your tests' behavior. Take note of the following changes: + +* Headless Chromium now automatically upgrades insecure HTTP requests to HTTPS. +* Headless Chromium does not always honor the `--window-size` flag. This behavior is a [known Chromium bug](https://issues.chromium.org/issues/40256833). + +## Bug Fixes + +1. [Native automation] TestCafe does not execute the `maximizeWindow()` method in beforeEach hooks ([#8117](https://github.com/DevExpress/testcafe/issues/8117)) +2. If TestCafe launch options include `--esm`, the framework crashes on launch in environments with Node.JS v20 and up ([#8132](https://github.com/DevExpress/testcafe/issues/8132)) +3. The Linux-based Docker image of TestCafe cannot run tests in headless Chromium ([#8145](https://github.com/DevExpress/testcafe/issues/8145)) +4. TestCafe incorrectly crops Safari screenshots ([#8154](https://github.com/DevExpress/testcafe/issues/8154)) + +## v3.5.0 (2023-12-26) + +TestCafe v3.5.0 includes multiple enhancements and bug fixes. Pass Selector queries to the Visual Selector Debugger, explore new ways to specify screenshot path patterns, and use a new *experimental* flag to run multi-window tests with native automation! + +meta-readmore + +### Pass Selector queries to the Visual Selector Debugger + +When you pass a Selector query to the [t.debug()](xref:402707) method, TestCafe uses the query to populate the input field of the Visual Selector Debugger. The debugger highlights page elements that match the query. + +```js +t.debug(Selector('#header')); +``` + +>[!Video https://www.screencast.com/users/testcafe/folders/Default/media/4274d757-f7a4-4982-add4-43bb0ba35cff/embed] + +### Use a custom path pattern for screenshots of failed tests + +The `pathPatternOnFails` [screenshot option](xref:402639#-s---screenshots-optionvalueoption2value2) allows TestCafe users to define a separate set of naming rules for screenshots taken on test failure. You can store these screenshots in a different folder, or add a common, recognizable element to their filenames. You can use this option on its own, or in conjunction with the `pathPattern` property. + +```json +{ + "screenshots": { + "pathPatternOnFails": "${DATE}_${TIME}/failedTests/test-${TEST_INDEX}/${USERAGENT}/${FILE_INDEX}.png" + } +} +``` + +### Specify a path pattern for individual screenshots + +Use the `pathPattern` option of the [t.takeScreenshot](xref:402675) action to specify a custom naming pattern for an individual screenshot: + +```js +t.takeScreenshot({ + pathPattern: "${DATE}_${TIME}/checkout-screenshot.png", + fullPage: true +}) +``` + +### (Experimental) Run multi-window tests with native automation + +TestCafe v2.5.0 was the first version of TestCafe to include [native automation](xref:404237) --- the capability to automate Chromium-based browsers with the native Chrome Debugging Protocol. This approach offers greater test stability and speed, but has a fair share of limitations. One of them is its incompatibility with multi-window tests. + +TestCafe v3.5.0 offers an experimental solution for this issue --- the [--experimental-multiple-windows](xref:402639#--experimental-multiple-windows) CLI flag. If you enable this flag, you can run multi-window tests with the native automation engine. + +The `--experimental-multiple-windows` mode does not support tests that include the following: + +* Pop-up windows that launch file downloads. +* Browser window resizing. +* Screenshots. +* Video recording. + +Please do not use the `--experimental-multiple-windows` flag in production or for business-critical tasks. + +### Bug Fixes + +* TypeScript compilation fails if project dependencies include '@babel/plugin-transorm-runtime' v7.23.3 or greater ([#8091](https://github.com/DevExpress/testcafe/issues/8091)). +* If you enable concurrent test execution, TestCafe launches tests before the conclusion of the `fixture.before` hook ([#6999](https://github.com/DevExpress/testcafe/issues/6999)). +* The `Fixture.disableConcurrency` method does not disable concurrent test execution ([8087](https://github.com/DevExpress/testcafe/issues/8087)). +* TestCafe ignores the fullPage option when it takes screenshots on test failure ([#7761](https://github.com/DevExpress/testcafe/issues/7761)). +* [Native Automation] TestCafe cannot populate file input fields with the `required` attribute ([#8079](https://github.com/DevExpress/testcafe/issues/8079)). +* [Native Automation] TestCafe fails to execute tests that use service workers ([#8005](https://github.com/DevExpress/testcafe/issues/8005), [#8054](https://github.com/DevExpress/testcafe/issues/8054)). +* When an action target is obscured by a sticky element, TestCafe incorrectly calculates the scroll distance necessary to interact with the target. ([#7377](https://github.com/DevExpress/testcafe/issues/7377)). +* Incorrect processing of front-end scripts causes automation errors ([#7713](https://github.com/DevExpress/testcafe/issues/7713), [#8067](https://github.com/DevExpress/testcafe/issues/8067), [testcafe-hammerhead#2969](https://github.com/DevExpress/testcafe-hammerhead/issues/2969)). +* TestCafe incorrectly processes failing network requests when it runs on Node.js v16 and greater ([#7097](https://github.com/DevExpress/testcafe/issues/7097)). +* TestCafe incorrectly handles native dialogs in Mozilla Firefox ([#6815](https://github.com/DevExpress/testcafe/issues/6815)). + +## v3.4.0 (2023-11-09) + +TestCafe v3.4.0 introduces relative Role URLs, the ability to disable concurrency on a per-fixture basis, as well as other improvements and bug fixes. + +meta-readmore + +### Enhancements + +### Relative Role URLs + +Earlier versions of TestCafe did not support relative URLs for [Role log-in pages](xref:402845). In TestCafe v3.4.0 and higher, if you set the [baseUrl](xref:402638#base-url) configuration file parameter or the [--base-url](xref:402639#--base-url) CLI option, you can set a relative URL for a Role log-in page: + +```js +import { Role } from 'testcafe'; + +const userOne = Role('./login', async t => { + /* log-in actions go here */ +}); +``` + +### Disable concurrency on a per-fixture basis + +[Concurrent test execution](xref:403626) is not suitable for tests that can only run in a certain order. To ignore the global concurrency setting for a particular fixture, use the [disableConcurrency](xref:404618) fixture method. + +```js +fixture`Fixture.disableConcurrency` + .page`https://devexpress.github.io/testcafe/example/` + .disableConcurrency; +``` + +### Development Mode Enhancements + +When you debug code inside a browser, the browser can appear unresponsive. Earlier versions of TestCafe automatically relaunched unresponsive browsers, including browsers that were used for debugging. + +TestCafe v3.4.0 *does not* relaunch unresponsive browsers if you enter [development mode](xref:402638#developmentmode). + +### Debug Panel Enhancements + +The [debug panel](xref:404288) includes a new "Hide Picker" button. Click this button to disable the Selector Debugger and hide the Selector input field. + +![Hide the Selector input field](https://testcafe.io/images/inspector/hide-selector-picker.gif) + +### Bug Fixes + +* TestCafe incorrectly logs requests during concurrent test execution ([#7977](https://github.com/DevExpress/testcafe/issues/7977)). +* TestCafe does not load images with non-lowercase `srcset` attribute declarations ([testcafe-hammerhead#2958](https://github.com/DevExpress/testcafe-hammerhead/issues/2958)). +* TestCafe raises an unexpected client-side error when the application opens an `ngx-formly` form ([#7758](https://github.com/DevExpress/testcafe/issues/7758)). +* TestCafe cannot interact with page items at the edge of the viewport when the browser emulates a mobile device ([#8057](https://github.com/DevExpress/testcafe/issues/8057)). + + +## v3.3.0 (2023-08-29) + +TestCafe v3.3.0 includes important bug fixes and quality of life improvements. + +### Bug Fixes + +* TestCafe terminates the test run when it attempts to parse an empty JSON file ([#7935](https://github.com/DevExpress/testcafe/issues/7935)). +* Firefox throws an unexpected error when TestCafe attempts to close the browser window ([#7285](https://github.com/DevExpress/testcafe/issues/7285)). +* [Native Automation] TestCafe ignores the `--disable-multiple-windows` option when you interact with a link that points to "target=_blank", or open a new window with the `window.open` method ([#7916](https://github.com/DevExpress/testcafe/issues/7916)). +* [Native Automation] TestCafe ignores the clientScripts directive when you mock HTTP requests ([#7914](https://github.com/DevExpress/testcafe/issues/7914)). +* [Native Automation] TestCafe hangs when it runs tests in the headless version of Google Chrome ([#7898](https://github.com/DevExpress/testcafe/issues/7898)). +* [Native Automation] TestCafe doesn't throw an error when the user attempts to enable the `userProfile` option ([#7925](https://github.com/DevExpress/testcafe/issues/7925)). + +## v3.2.0 (2023-08-17) + +TestCafe v3.2.0 allows you to check whether TestCafe uses native automation to control the browser. + +### Check your native automation status + +The `nativeAutomation` property of the [t.browser](https://testcafe.io/documentation/402712/reference/test-api/testcontroller/browser) object indicates whether TestCafe uses native automation to control the browser. The property's value is `true` when TestCafe uses native automation and `false` when TestCafe uses the Hammerhead proxy. + +You can check the browser's native automation status before you start the test: + +```js +import { Selector } from 'testcafe'; + +fixture`TestController.browser` + .page`https://example.com`; + +test('Native automation check', async t => { + await t.expect(t.browser.nativeAutomation).ok(); + //the test continues only if you use native automation +}); +``` + +### Bug Fixes + +* TestCafe uses a version of the `error-stack-parser` package that contains a vulnerable dependency ([PR #7919](https://github.com/DevExpress/testcafe/pull/7919) by [@sethidden](https://github.com/sethidden)). +* TestCafe does not clear cookie storage if a Role activation URL is the same as the page URL ([#7874](https://github.com/DevExpress/testcafe/issues/7874)). +* [Native Automation] TestCafe incorrectly processes web pages with file inputs ([#7886](https://github.com/DevExpress/testcafe/issues/7886)). + +## v3.1.0 (2023-07-27) + +TestCafe v3.1.0 introduces two enhancements: + +* You can now respond to geolocation requests with the `t.setNativeDialogHandler` method. +* Your tests and test reports can now reference a variable that stores the framework's version number. + +### Respond to geolocation requests + +> Main article: [t.setNativeDialogHandler](https://testcafe.io/documentation/402684/reference/test-api/testcontroller/setnativedialoghandler) + +Use the `t.setNativeDialogHandler` method to respond to `geolocation` requests. + +* Return an `Error` type object to **Block** geolocation requests. +* Return an object with [coordinates](https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPosition) to trigger the `success` callback of the [getCurrentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition) method. + +```js +// Test +test('Switch from "allow" to "block"', async t => { + await t + .setNativeDialogHandler((type) => { + if (type === 'geolocation') + return { timestamp: 12356, accuracy: 20, coords: {latitude: '34.15321262322903', longitude: '-118.25543996370723'}; // Passes this data to geolocation requests + return null; + }); + .click('#buttonGeo') + .setNativeDialogHandler((type) => { + if (type !== 'geolocation') + return null; + + const err = new Error('Some error'); + + err.code = 1; + + return err; // Blocks geolocation requests + }) + .click('#buttonGeo'); +``` + +### Reference the framework's version in tests and test reports + +> Main article: [Version Logger API](https://testcafe.io/documentation/404469/reference/version-logger-api) + +Earlier versions of TestCafe could output the framework's version number to the console: + +![CLI version](https://testcafe.io/images/testcafe-version.png) + +TestCafe 3.1.0 and up allows you to access the framework's version number in test code: + +```js +import { version } from 'testcafe'; +console.log(`TestCafe version: ${version}`); +``` + +![API version](https://testcafe.io/images/output-testcafe-version.png) + +To access the framework's version number in your custom reporter, reference the first argument (`version`) of the `init` method: + +```js +init (version) { + this + .write(`Using TestCafe ${version}`) + .newline() +} +``` + +### Bug fixes + +* TestCafe incorrectly reports test duration in concurrency mode ([#1816](https://github.com/DevExpress/testcafe/issues/1816)). +* TestCafe assigns a non-zero duration value to skipped tests, which leads to an unexpected increase in the total test run duration value ([#7731](https://github.com/DevExpress/testcafe/issues/7731)). +* [Native Automation] The `setFileUpload` method does not work ([#7832](https://github.com/DevExpress/testcafe/issues/7832)). +* [Native Automation] Request hooks cause tests to crash ([#7846](https://github.com/DevExpress/testcafe/issues/7846 +)). +* [Native Automation] TestCafe overrides page titles ([#7833](https://github.com/DevExpress/testcafe/issues/7833)). +* [Native Automation] If a website redirects the user to a new page before basic HTTP authentication is complete, the authentication process fails ([#7852](https://github.com/DevExpress/testcafe/issues/7852)). +* [Native Automation] The `t.click` action fails if the event handler accounts for [pointer input pressure](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pressure) ([#7867](https://github.com/DevExpress/testcafe/issues/7867)). +* [Native Automation] TestCafe hangs when the browser yields a "Session with given ID not found" error ([#7865](https://github.com/DevExpress/testcafe/issues/7865),[#7810](https://github.com/DevExpress/testcafe/issues/7810)). +* [Native Automation] TestCafe cannot set the `httpOnly` flag when you use the `t.setCookies` method ([#7793](https://github.com/DevExpress/testcafe/issues/7793)). + +## v3.0.1 (2023-06-29) + +### Bug fixes + +* The TestCafe status bar overlaps page elements, which leads to test execution issues ([#7797](https://github.com/DevExpress/testcafe/issues/7797)) +* TestCafe outputs an unhelpful warning message when it cannot apply the artifact path template ([#7256](https://github.com/DevExpress/testcafe/issues/7256)) +* A bug in the testcafe-browser-tools package causes TestCafe tests to hang on Ubuntu ([#7752](https://github.com/DevExpress/testcafe/issues/7752)) + +## v3.0.0 (2023-06-21) + +This major update includes two **breaking changes**: +* TestCafe v3.0.0 uses native CDP automation to run tests in Chromium-based browsers. +* TestCafe v3.0.0 removes support for Internet Explorer. + +Other changes include: +* You can now access test and fixture data in hooks. +* You can now dismiss the `print` dialog with the native dialog handler. + +### Native automation + +TestCafe v2.5.0 introduced an *experimental* mode that allows users to automate Chromium-based browsers, such as Google Chrome and Microsoft Edge, with the native CDP protocol. TestCafe v3.0.0 and up enables this capability out of the box. + +Native automation increases test quality, stability, and speed. + +* Read the ["TestCafe goes native"](https://testcafe.io/404431/resources/blog/2023-6-21-testcafe-goes-native) announcement for more information on the benefits of the new approach. +* Read the [Native Automation FAQ](https://testcafe.io/documentation/404237/guides/intermediate-guides/native-automation-mode) for more information on the practical aspects of this capability. + +### Access Test and Fixture data in hooks + +You can now access the following data in fixture hooks (`fixture.before`, `fixture.after`) : + +* Fixture name +* Fixture metadata +* Fixture path + +Test hooks (`fixture.beforeEach`, `fixture.afterEach`, `test.before`, `test.after`) can access fixture data **and** the following test data: + +* Test name +* Test metadata + +```js +fixture `Example Fixture` + .page `http://example.com` + .meta({ fixtureMeta: 'v' }) + .before( async (ctx, info) => { + const fixtureName = info.name; /* Example Fixture */ + const fixtureMeta = info.meta; /* { fixtureMeta: 'v' } */ + const fixturePath = info.path /* /Users/dan/testcafe/fixture.js */ + }); + .beforeEach( async t => { + const fixtureName = t.fixture.name; /* Example Fixture */ + const fixtureMeta = t.fixture.meta; /* { fixtureMeta: 'v' } */ + const fixturePath = t.fixture.path /* /Users/dan/testcafe/fixture.js */ + const testName = t.test.name; /* MyTest */ + const testMeta = t.test.meta; /* { 'key': 'value' } */ +}) +``` + +Read the [Hooks guide](https://testcafe.io/documentation/403435/guides/intermediate-guides/hooks#access-fixture-and-test-data-in-hooks) for more information. + +### Dismiss the print dialog + +You can now use the [t.setNativeDialogHandler](https://testcafe.io/documentation/402684/reference/test-api/testcontroller/setnativedialoghandler) method to dismiss the print dialog. + +### Removed: Internet Explorer support + +TestCafe v3.0.0 removes support for Internet Explorer 11, six months after the browser's official [retirement](https://techcommunity.microsoft.com/t5/windows-it-pro-blog/internet-explorer-11-desktop-app-retirement-faq/ba-p/2366549). The browser came out more than 9 years ago, and has a worldwide market of less than [0.5%](https://gs.statcounter.com/browser-market-share). It is survived by Edge, a popular Chromium-based browser that ships with modern versions of Windows. + +### Bug fixes + +* Some client functions yield a fatal error when the test navigates to a new page or removes an iframe ([#7707](https://github.com/DevExpress/testcafe/issues/7707)). +* TestCafe fails to correctly modify certain request headers when it uses native automation ([#7748](https://github.com/DevExpress/testcafe/issues/7748)). +* A bug in the CDP protocol causes TestCafe to incorrectly process request hooks ([#7743](https://github.com/DevExpress/testcafe/issues/7743)). +* TestCafe outputs a vague error message if the framework fails to read or process the configuration file ([#7208](https://github.com/DevExpress/testcafe/issues/7208), [#6437](https://github.com/DevExpress/testcafe/issues/6437)). +* TestCafe cannot select content with the "Ctrl+A" shortcut when the framework uses native automation ([#7667](https://github.com/DevExpress/testcafe/issues/7667)). +* The Monaco editor does not display code completion hints when TestCafe automates it with CDP [#7770](https://github.com/DevExpress/testcafe/issues/7770). + +## v2.6.2 (2023-06-01) + +TestCafe v2.6.2 introduces a number of bug fixes. + +### Bug fixes + +* Internet Explorer 11 hangs because it cannot process client-side scripts that ship with TestCafe v2.6.1 ([#7741](https://github.com/DevExpress/testcafe/issues/7741)). +* The `pressKey('space')` action doesn't affect checkbox status in Firefox ([#6969](https://github.com/DevExpress/testcafe/issues/6969)). + +## v2.6.1 (2023-05-29) + +TestCafe v2.6.1 retires **Experimental Debug** mode, and introduces a number of important bug fixes. + +### Removed: Experimental debug mode + +TestCafe v1.18.0 introduced [Experimental Debug mode](https://testcafe.io/403664/release-notes/framework/2021-12-22-testcafe-v1-18-0-released) --- a way to debug Selectors and Client Functions in the text editor. TestCafe v2.4.0 shipped with the [Visual Selector Debugger](https://testcafe.io/documentation/404288/guides/intermediate-guides/visual-selector-debugger), which allows users to troubleshoot Selector queries directly in the browser. + +The two capabilities serve the same purpose, but the Visual Selector Debugger is more user-friendly. As such, beginning with TestCafe v2.6.1, the framework **no longer includes** Experimental Debug mode. Thank you to all the TestCafe users who tried out the capability. + +### Bug fixes + +* When TestCafe runs in Native Automation mode, Request Hooks yield an error ([#7683](https://github.com/DevExpress/testcafe/issues/7683)). +* When TestCafe runs in Native Automation mode, the framework incorrectly processes pages with the pound sign ("#") in the URL ([#7652](https://github.com/DevExpress/testcafe/issues/7652)). +* TestCafe incorrectly handles XHR headers in Native Automation mode ([#7664](https://github.com/DevExpress/testcafe/issues/7664), [#7686](https://github.com/DevExpress/testcafe/issues/7686), [#7645](https://github.com/DevExpress/testcafe/issues/7645)). +* TestCafe reports an incorrect browser alias when it runs tests in Microsoft Edge ([#7647](https://github.com/DevExpress/testcafe/issues/7647)). +* TestCafe fails to intercept all HTTP requests when it runs in Native Automation mode. ([#7640](https://github.com/DevExpress/testcafe/issues/7640)). +* TestCafe cannot resize browser windows in the latest version of Chrome for macOS ([#7684](https://github.com/DevExpress/testcafe/issues/7684)). +* TestCafe incorrectly processes client-side styles, causing slowdowns and errors ([#6726](https://github.com/DevExpress/testcafe/issues/6726), [#6747](https://github.com/DevExpress/testcafe/issues/6747)). +* TestCafe crashes when you use the TestCafe Test Runner API to launch multiple tests simultaneously ([#7711](https://github.com/DevExpress/testcafe/issues/7711)). + +## v2.6.0 (2023-05-11) + +TestCafe v2.6.0 introduces two enhancements: a new hook that allows users to modify reporter output, and support for JavaScript configuration files with the `.cjs` extension. + +### New reporter hook + +The [onBeforeWrite](https://testcafe.io/documentation/404388/guides/advanced-guides/modify-reporter-output) hook allows you to modify the output of a reporter. + +If you want your test reports to include custom content, you can create a custom reporter from scratch. However, this approach takes time and effort. Use the `onBeforeWrite` hook if you want to make minor changes to the output of an existing reporter. + +Define an `onBeforeWrite` hook in a JavaScript configuration file. The following hook adds the duration in milliseconds to every test entry in the report: + +```js +//.testcaferc.js or .testcaferc.cjs +function onBeforeWriteHook(writeInfo) { // This function will fire every time the reporter calls the "write" method. + if (writeInfo.initiator === 'reportTestDone') { // The "initiator" property contains the name of the reporter event that triggered the hook. + const { + name, + testRunInfo, + meta + } = writeInfo.data || {}; // If you attached this hook to a compatible reporter (such as "spec" or "list"), the hook can process data related to the event. + const testDuration = testRunInfo.durationMs; // Save the duration of the test. + writeInfo.formattedText = writeInfo.formattedText + ' (' + testDuration + 'ms)'; // Add test duration to the reporter output. + }; +} + + +module.exports = { // Attach the hook + hooks: { + reporter: { + onBeforeWrite: { + 'spec': onBeforeWriteHook, // This hook will fire when you use the default "spec" reporter. + }, + }, + }, +}; +``` + +![Reporter hook demonstration](https://testcafe.io/images/reporter-hook.png) + +### CJS support + +If you run TestCafe v2.6.0 and higher, you can now use a configuration file with the `.cjs` file extension. TestCafe detects the `.testcaferc.cjs` file on startup, alongside its `.js` and `.json` counterparts. + +[TestCafe configuration files](https://testcafe.io/documentation/402638/reference/configuration-file) **only** support CommonJS syntax. Meanwhile, modern JavaScript tools often default to ESM syntax. If a JavaScript project is of type `module`, Node.js expects the project's `.js` files to contain ESM syntax. + +Use the `.cjs` configuration file extension to let Node.js know that the file contains CommonJS syntax. + +Many thanks to the TestCafe contributor Damien Guérin ([@gigaga](https://github.com/DevExpress/testcafe/pull/7614)) for the implementation of this capability. + +### Bug fixes + +* If you call the `t.skipJsErrors` method without arguments, TestCafe passes a `false` value to the method. This behavior is inconsistent with similar methods of a greater scope --- `test.skipJsErrors` and `fixture.skipJsErrors` ([#7648](https://github.com/DevExpress/testcafe/issues/7648)). +* Users cannot disable the "quarantine mode" or "skipJsErrors" settings from the command line ([#7077](https://github.com/DevExpress/testcafe/issues/7077)). +* TestCafe incorrectly processes exceptions of types other than `Error` ([#7627](https://github.com/DevExpress/testcafe/issues/7627)). +* TestCafe does not consistently execute the `t.pressKey` action in Mozilla Firefox. Attempts to press the "backspace" key and the "tab" key, among others, may fail. ([#7623](https://github.com/DevExpress/testcafe/pull/7623)) +* When TestCafe runs in Native Automation mode, it incorrectly executes some instances of the `t.request` method. ([#7609](https://github.com/DevExpress/testcafe/issues/7609)) +* The TestCafe proxy incorrectly processes private class properties in client-side scripts, which leads to page load failure ([#7632](https://github.com/DevExpress/testcafe/issues/7632), PR by [@sorin-davidoi](https://github.com/sorin-davidoi)). + +## v2.5.0 (2023-04-06) + +TestCafe v2.5.0 introduces three major enhancements: +* The new `t.report` method passes custom data to the test reporter. +* The new `--native-automation` flag enables TestCafe to automate all Chromium-based browsers with the native CDP protocol. +* The new `--esm` flag allows users to import ESM modules in test files. + +### t.report + +Include the [t.report()](https://testcafe.io/documentation/404350/reference/test-api/testcontroller/report) method in your test to pass custom data to the reporter. + +Specify arguments of any type (string, array, Object, etc). Separate arguments with a comma: + +```js +await + t.report( + 'text', + {'key': 'value'}, + ['arrayItem1', 'arrayItem2'] + ); +``` + +The default `spec` reporter displays custom data after test completion, once for each browser that runs the test. + +![Report with custom data](https://testcafe.io/images/treport.png) + +### CDP Automation: Now Stable + +TestCafe v2.2.0 introduced an experimental [proxyless mode](https://testcafe.io/documentation/404237/guides/intermediate-guides/native-automation-mode) that automated Google Chrome with the native CDP protocol. + +For the v2.5.0 release, the TestCafe team addressed most issues that our users discovered when the capability was "experimental", and gave it a new name --- Native Automation mode. + +Unlike its predecessor, the Native Automation mode supports **all** Chromium-based browsers, including Microsoft Edge. Enable the `nativeAutomation` option in the [command line interface](https://testcafe.io/documentation/402639/reference/command-line-interface#--native-automation), the [configuration file](https://testcafe.io/documentation/402638/reference/configuration-file#nativeautomation), or the [runner.run()](https://testcafe.io/documentation/402655/reference/testcafe-api/runner/run#nativeautomation) function to try this capability. + +> [!IMPORTANT] +> TestCafe v2.5.0 removed the `experimentalProxyless` option from the [createTestCafe](https://testcafe.io/documentation/402662/reference/testcafe-api/global/createtestcafe) function. Use the [runner.run()](https://testcafe.io/documentation/402655/reference/testcafe-api/runner/run#nativeautomation) function to enable Native Automation mode from the TestCafe Test Runner API. + +### ESM Module Support: Now Stable + +TestCafe v2.5.0 **drops** the `experimental` prefix from the `--esm` [CLI flag](https://testcafe.io/documentation/402639/reference/command-line-interface#--esm). Enable the `--esm` flag to import modules that do not support CommonJS. + +```sh +testcafe chrome test.js --esm +``` + +## v2.4.0 (2023-03-06) +TestCafe v2.4.0 introduces the Visual Selector Debugger. You can now create and debug Selector queries in the browser window. + +### Visual Selector Debugger + +TestCafe v2.4.0 displays the Visual Selector Debugger panel when you activate [Debug Mode](https://testcafe.io/documentation/402835/guides/basic-guides/debug-tests). Use the panel to debug Selector queries from your test, or generate new Selector queries. + +![](https://testcafe.io/images/inspector/enter-query.gif) + +If a Selector query causes your test to fail, add the [t.debug()](https://testcafe.io/documentation/402707/reference/test-api/testcontroller/debug) command after the last successful action, and launch the test. + +When the test reaches the breakpoint, the window that runs the test displays the Selector Debugger panel. Copy the failing Selector query from test code to the Selector Debugger input field. + +* TestCafe highlights page elements that match the Selector query. +* If no elements match the Selector query, the panel displays the **No Matching Elements** warning. +* If your Selector query contians a syntax error, the panel displays the **Invalid Selector** warning. + +To interactively generate a Selector query, click the **Pick** button, and select the target element on the page. + +For more information on the panel, its capabilities, and limitations, read the [Visual Selector Debugger Guide](https://testcafe.io/documentation/404288/guides/intermediate-guides/visual-selector-debugger). + +### Bug Fixes + +* TestCafe cannot execute the [t.request](https://testcafe.io/documentation/403981/reference/test-api/testcontroller/request) action in [proxyless mode](https://testcafe.io/documentation/404237/guides/experimental-capabilities/proxyless-mode) ([#7523](https://github.com/DevExpress/testcafe/issues/7523)). + +## v2.3.1 (2023-02-09) +TestCafe v2.3.1 introduces a number of bug fixes. + +### Bug Fixes + +* Client-side code with optional chaining may trigger a TestCafe error ([#7387](https://github.com/DevExpress/testcafe/issues/7387)). +* TestCafe cannot interact with images from the Shadow DOM ([#7454](https://github.com/DevExpress/testcafe/issues/7454)). +* TestCafe v2.3.0 fails to launch when the `test.meta` method precedes test code ([#7482](https://github.com/DevExpress/testcafe/issues/7482)). +* When TestCafe launches a headless instance of Google Chrome in proxyless mode, it cannot interact with elements that are overlapped by the status bar ([#7483](https://github.com/DevExpress/testcafe/issues/7483)). + +## v2.3.0 (2023-01-30) + +TestCafe v2.3.0 introduces `create-testcafe` --- an interactive tool that allows you to initialize a new TestCafe project in seconds. The update also includes *experimental* ECMAScript module support and a number of bug fixes. + +> **_IMPORTANT:_** +> TestCafe v2.3.0 ends support for Node.js 14 due to a known vulnerability in the `babel-plugin-module-resolver` module. +> +> Install an up-to-date version of the Node.js runtime to use TestCafe v2.3.0 and up. +> +> The official maintenance period for Node.js 14 [elapses](https://endoflife.date/nodejs) on April 1st, 2023. + + +### create-testcafe + +Use the [create-testcafe](https://github.com/devexpress/create-testcafe) tool to initialize a new TestCafe project, or *add* TestCafe to an existing Node.js application. + +Execute the following command to launch `create-testcafe`: + +```sh +npx create-testcafe +``` + +![example](https://testcafe.io/images/create-testcafe/wizard.gif) + +The `create-testcafe` tool allows you to perform the following actions with a single command: + +1. Create a new folder for the TestCafe project *(optional)*. +2. Create a new local installation of TestCafe and its dependencies. +3. Create and initialize a TestCafe configuration file. +4. Create a separate subfolder for tests. +5. Populate the test folder with test examples *(optional)*. +6. Create a YAML file with a GitLab Actions workflow that runs TestCafe tests *(optional)*. + +Read the [TestCafe Setup Wizard guide](https://testcafe.io/documentation/404259/guides/best-practices/create-testcafe) for more information on the create-testcafe tool. + +### Experimental: ECMAScript module support + +> **_IMPORTANT:_** +> ESM module suppport works with Node.js 16 and up. + +TestCafe has always used *CommonJS* syntax for module imports: + +```js +const { x } = require('y'); +``` + +An increasing number of Node.JS packages abandon CommonJS in favour of [ECMAScript module syntax](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): + +```js +import {x} from 'y' +``` + +Enable the `--experimental-esm` [CLI flag](https://testcafe.io/documentation/402639/reference/command-line-interface#--experimental-esm) to import modules that do not support CommonJS. Note: tests with ECMASCript module syntax are subject to [additional requirements](https://testcafe.io/404257/release-notes/framework/2023-1-30-testcafe-v2-3-0-released#additional-reuqirements). + +```sh +testcafe chrome test.js --experimental-esm +``` + +#### Additional Reuqirements + +To run tests with ECMAScript `import` statements, make sure that your project meets at least one of the following requirements: + +1. The value of the `type` key in your project's [package.json file](https://nodejs.org/api/packages.html#packagejson-and-file-extensions) is `module`. +2. The test files in your project use the `.mjs` extension. + +### Bug Fixes + +* TestCafe doesn't delete expired cookies ([#7432](https://github.com/DevExpress/testcafe/issues/7432)). +* TestCafe cannot handle windows that appear when the user clicks a link with a `_blank` target ([#6926](https://github.com/DevExpress/testcafe/issues/6926)). +* TestCafe fails to start because it triggers the `dns.setDefaultResultOrder` method in older Node.js environments ([#7447](https://github.com/DevExpress/testcafe/issues/7447)). +* TestCafe depends on a vulnerable, outdated version of the `babel-plugin-module-resolver` package ([#7456](https://github.com/DevExpress/testcafe/issues/7456)). + +## v2.2.0 (2022-12-29) + +TestCafe v2.2.0 introduces user-defined custom actions and an important *experimental* capability. Google Chrome users can now enable "proxyless mode" to speed up their test suite. + +### Custom Action Support + +TestCafe users can now define [custom test actions](xref:404150). Place the definition function in a [**JavaScript** configuration file](xref:402638#customactions): + +```js +module.exports = { + customActions: { + async makeCoffee (args) { + await this.click(args); + }, + } +}; +``` + +Include custom methods in your tests alongside [other TestController methods](xref:402632#test-controller-api). +Add the `customActions` prefix when you call the action: + +```js +test('Test with a custom action', async t => { + await t.click() + .customActions.makeCoffee() + .click(); +}) +``` + +### Experimental: Proxyless mode + +TestCafe runs an under-the-hood [reverse proxy](xref:402631#page-proxying) to automate tests across different browsers. But this technique complicates the framework. Native automation protocols offer superior automation speeds and greater stability. That's why the TestCafe team decided to gradually phase the reverse proxy out in favor of native support for these automation protocols. + +TestCafe v2.2.0 includes an **experimental** option that disables the proxy for **Google Chrome**. + +```sh +testcafe chrome tests --experimental-proxyless +``` + +You can enable this option in the [command line interface](xref:402639#--experimental-proxyless), the [Test Runner API](xref:402662), and the [configuration file](xref:402638#experimentalproxyless). Read the [Proxyless mode guide](xref:404237) for more information. + +### Bug Fixes + +* TestCafe doesn't hide the live mode status bar when the bar obstructs the action target ([#7384](https://github.com/DevExpress/testcafe/issues/7384)) +* The 'Target element is overlapped' message does not reference the Selector that caused the warning ([#7386](https://github.com/DevExpress/testcafe/issues/7386)) +* The TestCafe Dashboard reporter includes an outdated `uuid` dependency ([testcafe-reporter-dashboard#111](https://github.com/DevExpress/testcafe-reporter-dashboard/issues/111)) +* TestCafe doesn't display the correct error message when the framework throws an exception ([#6936](https://github.com/DevExpress/testcafe/issues/6936)) +* TestCafe retains some cookies after the user requests their deletion (PR [testcafe-hammerhead#2818](https://github.com/DevExpress/testcafe-hammerhead/pull/2818)) +* TestCafe cannot load test pages with the `localhost` URL on Node.js v17 and up ([#7396](https://github.com/DevExpress/testcafe/issues/7396)) +* TestCafe cannot take screenshots in headless Chrome on Node.js v17 and up ([#7408](https://github.com/DevExpress/testcafe/issues/7408)) +* Web workers that originate from Blob URLs throw an error when they call the `importScript` function ([#7378](https://github.com/DevExpress/testcafe/issues/7378)) +* TestCafe doesn't set the correct `Request` header when an `iframe` points the user to a new URL ([#7376](https://github.com/DevExpress/testcafe/issues/7376), PR [testcafe-hammerhead#2813](https://github.com/DevExpress/testcafe-hammerhead/pull/2813) by [@naggingant](https://github.com/naggingant)) +* TestCafe cannot interact with options that belong to a `` elements ([#5616](https://github.com/DevExpress/testcafe/issues/5616)) +* TestCafe does not load some cross-domain iframes ([#6633](https://github.com/DevExpress/testcafe/issues/6633)) +* TestCafe incorrectly sets the Document.referrer property in Chrome 89 ([#6144](https://github.com/DevExpress/testcafe/issues/6144)) +* Tests hang when the test page initiates a file download ([#5796](https://github.com/DevExpress/testcafe/issues/5796)) +* Requests fail because TestCafe incorrectly handles dynamic content security policy ([#6057](https://github.com/DevExpress/testcafe/issues/6057)) +* TestCafe triggers pointerdown event handlers twice ([#5891](https://github.com/DevExpress/testcafe/issues/5891)) +* TestCafe cannot trigger click event handlers for Angular buttons with the "disabled" attribute ([#5240](https://github.com/DevExpress/testcafe/issues/5240)) + +## v1.18.1 (2021-12-23) + +### macOS Bug Fix + +TestCafe fails to launch Safari after the v1.18.0 update. + +## v1.18.0 (2021-12-22) + +TestCafe v1.18.0 includes a new experimental Selector debugging capability, important improvements for macOS users and a number of routine bug fixes. + +If you run TestCafe on macOS, follow the [Upgrade Guide](https://testcafe.io/403664/release-notes/framework/2021-12-22-testcafe-v1-18-0-released#upgrade-instructions) to make sure your upgrade goes smoothly. + +### New Debugging Capabilities (Experimental) + +If you launch TestCafe with the `--experimental-debug` flag, you can debug Selectors and Client Functions in the Watch panel of a Node.js debugger. + +### macOS improvements + +#### TestCafe Browser Tools on Apple Silicon + +The TestCafe Browser Tools package is a communication layer that automates browsers on behalf of TestCafe. Both the TestCafe framework and TestCafe Studio include the TestCafe Browser Tools binary. + +Earlier versions of TestCafe Browser Tools were optimized for the x86-64 architecture. [Apple Silicon Macs](https://support.apple.com/en-gb/HT211814/) ran those binaries through the [Rosetta 2 translation layer](https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment/). Rosetta 2 took up additional space and prevented TestCafe from taking full advantage of the processor. + +TestCafe v1.18.0 includes a **Universal** TestCafe Browser Tools binary that runs natively on both x64 Macs **and** Apple Silicon Macs. + +Follow the [Upgrade Instructions](https://testcafe.io/403664/release-notes/framework/2021-12-22-testcafe-v1-18-0-released#upgrade-instructions) to make sure your version of TestCafe Browser Tools is up to date. + +#### TestCafe Browser Tools macOS Permission Fix + +The TestCafe Browser Tools binary requires special privileges to automate browsers and take screenshots. Security improvements in recent versions of macOS made these privileges harder to obtain. + +Prior to TestCafe v1.18.0, each installation of TestCafe and TestCafe Studio included a TestCafe Browser Tools binary. macOS users with multiple sets of TestCafe Browser Tools had to go through a lengthy process to obtain the necessary permissions. + +TestCafe v1.18.0 and TestCafe Studio v1.7 address this issue. Beginning with this version, all TestCafe installations share a single TestCafe Browser Tools binary. TestCafe stores this binary in the user's Home directory, inside the hidden `~/.testcafe-browser-tools` folder. + +Follow the [Upgrade Instructions](https://testcafe.io/403664/release-notes/framework/2021-12-22-testcafe-v1-18-0-released#upgrade-instructions) to reset TestCafe Browser Tools' permissions and enable the new binary. + +### Bug Fixes + +* TestCafe immediately closes new windows ([#6680](https://github.com/DevExpress/testcafe/issues/6680)) +* Tests fail with the `TypeError: Invalid value used as weak map key.` error ([#6563](https://github.com/DevExpress/testcafe/issues/6563)) +* The latest version of the TestCafe Docker image cannot connect to Chrome and Chromium ([#6436](https://github.com/DevExpress/testcafe/issues/6436)) +* TestCafe loses test error call stack and outputs the following message instead: `"Uncaught object "[object Object]"` (Issue [#6624](https://github.com/DevExpress/testcafe/issues/6624). Discovered by [@danieltroger](https://github.com/DevExpress/testcafe/issues/6624#issuecomment-975918371), PR by [@rob4629](https://github.com/DevExpress/testcafe/pull/6719).) +* Lack of definitions for two new timeout options results in TypeScript compilation errors ([#6713](https://github.com/DevExpress/testcafe/issues/6713)) +* TypeScript filter functions erroneously require a Promise return value ([#6705](https://github.com/DevExpress/testcafe/issues/6705)) + +## v1.17.1 (2021-11-11) + +### Bug Fixes + +- TestCafe incorrectly reads the 'reporter' configuration file option ([#6665](https://github.com/DevExpress/testcafe/issues/6665), [#6594](https://github.com/DevExpress/testcafe/issues/6594)). +- An error report displays multiple warnings when you debug a test in headless mode ([#6605](https://github.com/DevExpress/testcafe/issues/6605)). +- The testcafe-hammerhead proxy fails to load a web page ([testcafe-hammerhead/#2708](https://github.com/DevExpress/testcafe-hammerhead/issues/2708)). + +## v1.17.0 (2021-11-02) + +### Enhancements + +#### Global Test and Fixture Hooks + +You can now specify [global test and fixture hooks](https://testcafe.io/documentation/403435/guides/advanced-guides/hooks#global-hooks). TestCafe attaches these hooks to every test / fixture in the test suite. + +```js +module.exports = { + hooks: { + fixture: { + before: async (ctx) => { + // your code + }, + after: async (ctx) => { + // your code + } + }, + test: { + before: async (t) => { + // your code + }, + after: async (t) => { + // your code + } + } + } +}; +``` + +#### Execution Timeouts + +You can now specify custom timeouts for tests and test runs. If a test/test run is idle or unresponsive for the specified length of time, TestCafe terminates it. Specify these timeouts in the [configuration file](https://testcafe.io/documentation/402638/reference/configuration-file) or from the [command line](https://testcafe.io/documentation/402639/reference/command-line-interface). + +**Command line interface** + +```sh +testcafe chrome my-tests --test-execution-timeout 180000 +testcafe chrome my-tests --run-execution-timeout 180000 +``` +**Configuration file** + +```json +{ + "runExecutionTimeout": 180000, + "testExecutionTimeout": 180000 +} +``` + +### Bug Fixes + +- TestCafe fails to continue the test after the user downloads a file. ([#6242](https://github.com/DevExpress/testcafe/issues/6242)). +- The TestCafe proxy does not fire the "unpipe" event when necessary. This omission leads to the "This socket has been ended by the other party" error ([#6558](https://github.com/DevExpress/testcafe/issues/6558)). +- TestCafe incorrectly handles rewritten uninitialized iframes ([testcafe-hammerhead/#2694](https://github.com/DevExpress/testcafe-hammerhead/issues/2694), [testcafe-hammerhead/#2693](https://github.com/DevExpress/testcafe-hammerhead/issues/2693)). + +## v1.16.1 (2021-10-05) + +### Bug Fixes + +* Incorrect handling of the beforeInput Firefox event ([#6504](https://github.com/DevExpress/testcafe/issues/6504)) +* Incorrect handling of page styles leads to test failure in Safari 15 ([#6546](https://github.com/DevExpress/testcafe/issues/6546)) +* Incorrect stylesheet filtering procedure leads to client-side errors in IE11 ([#6439](https://github.com/DevExpress/testcafe/issues/6439)) + +## v1.16.0 (2021-09-08) + +### Enhancements + +#### Support for JavaScript configuration files + +You can now store TestCafe settings in a `js` file. Configuration properties in JavaScript files can reference JavaScript methods, functions and variables, which makes it easy to create dynamic configuration files. + +Just `export` the JSON name/value pairs in the file: + +```js +module.exports = { + skipJsErrors: true, + hostname: "localhost", + // other settings +} +``` + +#### Support for custom user variables in the configuration file + +TestCafe v1.16.0 and later supports configuration files with variable declarations. Users can reference variables from a configuration file in the tests that utilize that configuration file. To enable access to configuration file variables, import the `userVariables` object from the `testcafe` module at the beginning of the test script. + +This capability can come in handy if there's a single piece of data you want to use in multiple tests — for example, the website's URL. That way, if your website moves to a new domain name, you don't have to change your tests one by one. + +If you previously used environment variables to achieve the same goal, you might prefer the new method — it significantly simplifies the setup process, and allows you to commit the data to a version control system. + +Define your custom variables with the `userVariables` JSON object: + +```JSON +{ + "userVariables": { + "url": "http://devexpress.github.io/testcafe/example", + } +} +``` + +Reference this variable in your test: + +```js +import { userVariables } from 'testcafe'; + +fixture `Test user variables` + .page(userVariables.url); + +test('Type text', async t => { + await t + .typeText('#developer-name', 'John Smith') + .click('#submit-button'); +}); +``` + +#### Other enhancements + +* New option that disables thumbnail generation for test screenshots ([PR by @taki-fw](https://github.com/DevExpress/testcafe/pull/6078)). +* New `embedding-utils` API method that retrieves information about skipped tests ([PR by @flora8984461](https://github.com/DevExpress/testcafe/pull/6398)). +* The `Runner.filter` function supports asynchronous arguments ([PR by @eignatyev](https://github.com/DevExpress/testcafe/pull/6371)). +* You can import the `test` and `fixture` objects directly from the `testcafe` module ([PR #6338](https://github.com/DevExpress/testcafe/pull/6338)). + +### Bug Fixes + +* TestCafe does not keep track of file changes in live mode ([#6481](https://github.com/DevExpress/testcafe/issues/6481)). + +## v1.15.3 (2021-08-19) + +### Bug Fixes + +* TestCafe throws an error if you use the 'all' alias in the command line to run tests in all installed browsers ([#6456](https://github.com/DevExpress/testcafe/issues/6456)). +* TestCafe does not check if the configuration file exists ([#6337](https://github.com/DevExpress/testcafe/issues/6337)). +* TestCafe cannot disable HTTP/2 to avoid compatibility issues ([testcafe-hammerhead/#2681](https://github.com/DevExpress/testcafe-hammerhead/pull/2681)). +* TestCafe works incorrectly if you use extended Function objects ([testcafe-hammerhead/#2439](https://github.com/DevExpress/testcafe-hammerhead/issues/2439)). +* TestCafe processes a test application incorrectly if it uses the Immutable.js library ([testcafe-hammerhead/#2669](https://github.com/DevExpress/testcafe-hammerhead/issues/2669)). +* TestCafe adds an element incorrectly into a nested body element ([PR testcafe-hammerhead/#2682](https://github.com/DevExpress/testcafe-hammerhead/pull/2682)). + +## v1.15.2 (2021-08-11) + +### Bug Fixes + +* TestCafe fails to emulate the `tab` action if a page contains a cross-domain iframe ([#6405](https://github.com/DevExpress/testcafe/issues/6405)). +* TestCafe ignores the quarantine mode options if you specify the configuration file ([#6420](https://github.com/DevExpress/testcafe/issues/6420)). +* TestCafe test fails if you specify a custom option as a command line argument ([#6426](https://github.com/DevExpress/testcafe/issues/6426)) +* The [ERR_HTTP2_GOAWAY_SESSION](https://nodejs.org/api/errors.html#errors_err_http2_goaway_session) error occurs randomly when you use HTTP/2 protocol ([testcafe-hammerhead/#2653](https://github.com/DevExpress/testcafe-hammerhead/issues/2653)). +* TestCafe fails if a page contains overridden getters for target elements ([testcafe-hammerhead/#2662](https://github.com/DevExpress/testcafe-hammerhead/issues/2662)). +* TestCafe incorrectly processes scripts that destruct empty elements ([testcafe-hammerhead/#2670](https://github.com/DevExpress/testcafe-hammerhead/issues/2670)). + +## v1.15.1 (2021-07-28) + +### Bug Fixes + +* The `Element.getAttribute` method returns an incorrect value ([#5984](https://github.com/DevExpress/testcafe/issues/5984)). +* TestCafe test fails when you forget to include the `await` keyword before the assertion statement ([#4613](https://github.com/DevExpress/testcafe/issues/4613)). +* TestCafe fails to focus an element inside a shadow DOM ([#4988](https://github.com/DevExpress/testcafe/issues/4988)). +* TestCafe fails to focus SVG elements ([#6262](https://github.com/DevExpress/testcafe/issues/6262)). +* TestCafe raises the blur event when you focus a non-focusable element ([#6236](https://github.com/DevExpress/testcafe/pull/6236)). +* TestCafe test hangs when you click a link within a cross-domain iframe ([#6331](https://github.com/DevExpress/testcafe/pull/6331)). +* TestCafe loads the Babel compiler libraries multiple times ([#6310](https://github.com/DevExpress/testcafe/pull/6310)). +* TestCafe incorrectly parses the meta refresh tags ([PR testcafe-hammerhead/#2663](https://github.com/DevExpress/testcafe-hammerhead/pull/2663)) +* TestCafe incorrectly processes iframe elements with the "srcdoc" attribute ([testcafe-hammerhead/#2647](https://github.com/DevExpress/testcafe-hammerhead/issues/2647)). +* TestCafe incorrectly specifies the Referer HTTP request header if you use the "navigateTo" action ([testcafe-hammerhead/#2607](https://github.com/DevExpress/testcafe-hammerhead/issues/2607)). +* An error related to the [bug in Node.js](https://github.com/nodejs/node/issues/37849) occurs ([testcafe-hammerhead/#2655](https://github.com/DevExpress/testcafe-hammerhead/pull/2655)). + +## v1.15.0 (2021-07-08) + +### Enhancements + +#### Dispatch DOM events ([PR #6103](https://github.com/DevExpress/testcafe/pull/6103)) + +```plaintext +t.dispatchEvent(target, eventName[, options]) +``` + +The [`t.dispatchEvent`](https://testcafe.io/documentation/402712/reference/test-api/testcontroller/dispatchevent) method lets you interact with the page in ways that TestCafe does not support out of the box. To implement an unsupported user action, break it down into discrete DOM events, and use the `t.dispatchEvent` method to fire them. + +>Internet Explorer does [not](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) support event constructors. As such, TestCafe cannot dispatch DOM events in this browser. + +The following example fires a `touchstart` action on an element with the 'button' id: + +```js +await t.dispatchEvent('#button', 'touchstart', { eventConstructor: 'TouchEvent' }); +``` + +Read the [Custom Actions Guide](https://testcafe.io/documentation/402843/guides/advanced-guides/custom-actions) for more information on DOM events and event constructors. + +#### Quarantine mode customization ([PR #6073](https://github.com/DevExpress/testcafe/pull/6073) by @rob4629) + +New settings are available in [quarantine mode](https://testcafe.io/documentation/402830/guides/basic-guides/run-tests#quarantine-mode). Quarantine mode repeats failing tests to help users get conclusive test results in sub-optimal conditions. TestCafe v1.15 adds two variables - `successThreshold` and `attemptLimit` - that allow you specify when TestCafe must stop. + +The `attemptLimit` variable determines the maximum possible number of test attempts. +The `successThreshold` variable determines the number of successful attempts necessary for the test to pass. + +```js +testcafe chrome ./tests/ -q attemptLimit=5, successThreshold=2 +``` + +#### Password obfuscation ([#6014](https://github.com/DevExpress/testcafe/issues/6014)) + +TestCafe reporters no longer receive the contents of `password` input fields, unless you explicitly specify otherwise. This improves security for users that store their test results online. + +#### Support for non-HTML documents ([#1471](https://github.com/DevExpress/testcafe/issues/1471)) + +TestCafe now has the capability to proxy non-HTML documents such as XML and text files. Tests no longer hang upon redirection to a non-HTML address. + +### Bug Fixes + +* TestCafe doesn't raise an error if users specify the CDP port but do not enable concurrency ([PR #6268](https://github.com/DevExpress/testcafe/pull/6268)). +* TestCafe incorrectly processes elements with negative tabIndex values ([#4848](https://github.com/DevExpress/testcafe/issues/4848)). +* TestCafe incorrectly processes some eventListeners in multi-window mode ([#5621](https://github.com/DevExpress/testcafe/issues/5621)). +* TestCafe incorrectly processes the \ HTML tag ([testcafe-hammerhead/#1965](https://github.com/DevExpress/testcafe-hammerhead/issues/1965)). +* TestCafe doesn't intercept `Navigator.prototype` requests ([PR testcafe-hammerhead/#2643](https://github.com/DevExpress/testcafe-hammerhead/pull/2643) by [@michaelficarra](https://github.com/michaelficarra)). +* TestCafe doesn't intercept `WorkerGlobalScope.importScripts()` arguments ([testcafe-hammerhead/#2521](https://github.com/DevExpress/testcafe-hammerhead/issues/2521)). +* A website parsing error causes test failure ([testcafe-hammerhead/#2650](https://github.com/DevExpress/testcafe-hammerhead/issues/2650)). +* TestCafe stops recording test videos after you skip a fixture ([#6163](https://github.com/DevExpress/testcafe/issues/6163)). +* Links with empty `download` attributes cause TestCafe to hang ([#6132](https://github.com/DevExpress/testcafe/issues/6132)). +* TestCafe reports incorrect line numbers ([#5642](https://github.com/DevExpress/testcafe/issues/5642)). +* TestCafe incorrectly processes some for-of statements ([PR testcafe-hammerhead/#2632](https://github.com/DevExpress/testcafe-hammerhead/pull/2632)). +* TestCafe sometimes directs window location queries to non-window objects ([testcafe-hammerhead/#2611](https://github.com/DevExpress/testcafe-hammerhead/issues/2611)). +* Performance enhancement: obtaining element attributes ([#6117](https://github.com/DevExpress/testcafe/issues/6117)) + +## v1.14.2 (2021-05-13) + +## Bug Fixes + +* Fixed a bug that caused the `The "--quarantine-mode" option value is not a valid key-value pair` error + +## v1.14.1 (2021-05-12) + +## Bug Fixes + +* Fixed a bug that caused TestCafe to hang during the execution of TestCafe Studio tests ([#5207](https://github.com/DevExpress/testcafe/issues/5207)) +* Added a type definition for the `disableScreenshots` Runner option ([#5735](https://github.com/DevExpress/testcafe/issues/5735)) +* Fixed a multi-window mode bug that caused TestCafe to launch an empty browser window and then hang ([#6132](https://github.com/DevExpress/testcafe/issues/6132)) +* Fixed a bug that denied users access to iframes with `srcdoc` attributes ([#6033](https://github.com/DevExpress/testcafe/issues/6033)) +* Fixed a bug that interfered with the loading of Word Online documents in Firefox ([testcafe-hammerhead/#2287](https://github.com/DevExpress/testcafe-hammerhead/issues/2287)) + +## v1.14.0 (2021-4-7) + +## Enhancements + +### ⚙ Scroll Actions ([PR #6066](https://github.com/DevExpress/testcafe/pull/6066)) + +When TestCafe interacts with elements on the page, it scrolls the page automatically to reach those elements. + +This release introduces actions that allow you to scroll webpage elements manually. + +* [t.scroll](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/scroll.html) - scrolls the element to a specified position +* [t.scrollBy](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/scrollby.html) - scrolls the element by the specified number of pixels +* [t.scrollIntoView](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/scrollintoview.html) - scrolls the element into view + +You can use the `t.scroll` action to scroll an element to a position: + +```js + import { Selector } from 'testcafe'; + + fixture`Scroll Action` + .page('http://example.com'); + + test('Scroll the container', async t => { + const container = Selector('#container'); + + await t + .scroll(container, 'bottomRight') + }); + ``` + + `t.scrollBy` allows you to scroll an element (or the webpage) by a set amount of pixels. The example below scrolls the webpage 200px up and 500px to the right: + + ```js + fixture`Scroll Action` + .page('http://example.com'); + + test('Scroll the webpage', async t => { + await t + .scrollBy(500, -200) + }); + ``` + +Use `t.scrollIntoView` to scroll an element into view: + +```js + import { Selector } from 'testcafe'; + + fixture `Scroll Actions` + .page `http://www.example.com/`; + + test('Scroll element into view', async t => { + const target = Selector('#target') + + await t + .scrollIntoView(target) + }); + ``` + +## Bug Fixes + +* Fixed an error that caused [expect.contains](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/expect/contains.html) assertions to display `undefined` instead of a string value in diffs ([#5473](https://github.com/DevExpress/testcafe/issues/5473)) + +## v1.13.0 (2021-03-22) + +This release adds support for custom paths to the configuration file, support for Microsoft Edge on Linux systems, and multiple bugfixes. + +### Enhancements + +#### :gear: Specify Custom Path to the TestCafe Configuration File ([PR #6035](https://github.com/DevExpress/testcafe/pull/6035) by [@Chris-Greaves](https://github.com/Chris-Greaves)) + +TestCafe now allows you to specify a custom [configuration file](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html) path. + +To set this path, use one of the following options: + +* the [--config-file CLI flag](https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--config-file-path) +* the [configFile parameter of the createTestCafe function](https://devexpress.github.io/testcafe/documentation/reference/testcafe-api/global/createtestcafe.html#options) + +#### Add Support for Microsoft Edge on Linux ([PR testcafe-browser-tools/#210](https://github.com/DevExpress/testcafe-browser-tools/pull/210) by [@dcsaszar](https://github.com/dcsaszar)) + +If you follow the [Microsoft Edge Insider Channels for Linux](https://www.microsoftedgeinsider.com/en-us/download?platform=linux-deb) and have Microsoft Edge installed on your Linux machine, you can now launch TestCafe tests in this browser. + +```sh +testcafe edge tests/test.js +``` + +#### :gear: Deprecated the `t.setPageLoadTimeout` method ([PR #5979](https://github.com/DevExpress/testcafe/pull/5979)) + +Starting with v1.13.0, the [t.setPageLoadTimeout](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/setpageloadtimeout.html) method is deprecated. To set the page load timeout, use the new [test.timeouts](https://devexpress.github.io/testcafe/documentation/reference/test-api/test/timeouts.html) method. + +```js +fixture`Setting Timeouts` + .page`http://devexpress.github.io/testcafe/example`; + +test + .timeouts({ + pageLoadTimeout: 2000 + }) + ('My test', async t => { + //test actions + }) +``` + +You can also use `test.timeouts` to set the [pageRequestTimeout](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#pagerequesttimeout) and [ajaxRequestTimeout](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#ajaxrequesttimeout). + +```js +fixture`Setting Timeouts` + .page`http://devexpress.github.io/testcafe/example`; + +test + .timeouts({ + pageLoadTimeout: 2000, + pageRequestTimeout: 60000, + ajaxRequestTimeout: 60000 + }) + ('My test', async t => { + //test actions + }) +``` + +### Bug Fixes + +* Fixed a bug where TestCafe would sometimes be unable to trigger a `hover` event on a `radio` element ([#5916](https://github.com/DevExpress/testcafe/issues/5916)) +* Fixed a bug where TestCafe was unable to register a Service Worker due to the wrong `currentScope` calculation inside a `Window.postMessage` call ([testcafe-hammerhead/#2524](https://github.com/DevExpress/testcafe-hammerhead/issues/2524)) +* `RequestLogger` now shows a correct protocol for WebSocket requests ([testcafe-hammerhead/#2591](https://github.com/DevExpress/testcafe-hammerhead/issues/2591)) +* Test execution now pauses when the browser window is in the background ([testcafe-browser-tools/#158](https://github.com/DevExpress/testcafe-browser-tools/issues/158)) +* TestCafe now appends an extension to screenshot filenames ([#5103](https://github.com/DevExpress/testcafe/issues/5103)) +* Fixed a bug where TestCafe would emit test action events after the end of a test run ([#5650](https://github.com/DevExpress/testcafe/issues/5650)) +* TestCafe now closes if the `No tests to run` error occurs in Live mode ([#4257](https://github.com/DevExpress/testcafe/issues/4257)) +* Fixed a freeze that happened when you run a test suite with skipped tests ([#4967](https://github.com/DevExpress/testcafe/issues/4967)) +* Fixed an error where a `documentElement.transform.translate` call moved the TestCafe UI in the browser window ([#5606](https://github.com/DevExpress/testcafe/issues/5606)) +* TestCafe now emits a warning if you pass an unawaited selector to an assertion ([#5554](https://github.com/DevExpress/testcafe/issues/5554)) +* Fixed a crash that sometimes occurred in Chrome v85 and earlier on pages with scripts ([PR testcafe-hammerhead/#2590](https://github.com/DevExpress/testcafe-hammerhead/pull/2590)) + +## v1.12.0 (2021-03-03) + +### Enhancements + +#### :gear: Server-Side Web Assets Caching ([testcafe-hammerhead/#863](https://github.com/DevExpress/testcafe-hammerhead/issues/863)) + +TestCafe's proxy server can now cache web assets (like images, scripts and videos). When TestCafe revisits a website, it loads assets from this cache to save time on repetetive network requests. + +To enable server-side caching, use any of the following: + +* [the `--cache` CLI flag](https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--cache) +* [the `cache` configuration file property](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#cache) +* [the `createTestCafe` function parameter](https://devexpress.github.io/testcafe/documentation/reference/testcafe-api/global/createtestcafe.html) + +#### Initialize Request Hooks with Async Predicates + +The following request hooks now support **asynchronous** predicate functions: + +* [RequestHook](https://devexpress.github.io/testcafe/documentation/reference/test-api/requesthook/constructor.html#filter-with-a-predicate) +* [RequestMock.onRequestTo](https://devexpress.github.io/testcafe/documentation/reference/test-api/requestmock/onrequestto.html#filter-with-a-predicate) +* [RequestLogger](https://devexpress.github.io/testcafe/documentation/reference/test-api/requestlogger/constructor.html#filter-with-a-predicate) + +**Example** + +```js +const logger = RequestLogger(async request => { + return await myAsyncFunction(); +}); +``` + +### Bug Fixes + +* Fixed a bug in Multiple Windows mode where TestCafe was sometime unable to switch to the main browser window ([#5930](https://github.com/DevExpress/testcafe/issues/5930)) +* Fixed the `Illegal invocation` error thrown by TestCafe when calling `Storage.prototype` methods on a `StorageWrapper` object ([#2526](https://github.com/DevExpress/testcafe-hammerhead/issues/2526)) + +## v1.11.0 (2021-03-02) + +### Enhancements + +#### :gear: Set Request Timeouts ([PR #5692](https://github.com/DevExpress/testcafe/pull/5692)) + +TestCafe now enables you to set request timeouts. If TestCafe receives no response within the specified period, it throws an error. + +*CLI* + +* [--ajax-request-timeout](https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--ajax-request-timeout-ms) controls the timeout for fetch/XHR requests +* [--page-request-timeout](https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--page-request-timeout-ms) sets the timeout for webpage requests + +```sh +testcafe chrome my-tests --ajax-request-timeout 40000 --page-request-timeout 8000 +``` + +*Configuration file* + +* [ajaxRequestTimeout](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#ajaxrequesttimeout) +* [pageRequestTimeout](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#pagerequesttimeout) + +```json +{ + "pageRequestTimeout": 8000, + "ajaxRequestTimeout": 40000 +} +``` + +*JavaScript API* + +These options are available in the [runner.run Method](https://devexpress.github.io/testcafe/documentation/reference/testcafe-api/runner/run.html). + +```js +const createTestCafe = require('testcafe'); + +const testcafe = await createTestCafe('localhost', 1337, 1338); + +try { + const runner = testcafe.createRunner(); + + const failed = await runner.run({ + pageRequestTimeout: 8000, + ajaxRequestTimeout: 40000 + }); + + console.log('Tests failed: ' + failed); +} +finally { + await testcafe.close(); +} +``` + +#### :gear: Set Browser Initialization Timeout ([PR #5720](https://github.com/DevExpress/testcafe/pull/5720)) + +This release introduces an option to control browser initialization timeout. This timeout controls the time browsers have to connect to TestCafe before an error is thrown. You can control this timeout in one of the following ways: + +* [--browser-init-timeout](https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--browser-init-timeout-ms) CLI option + +```sh +testcafe chrome my-tests --browser-init-timeout 180000 +``` + +* [browserInitTimeout](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#browserinittimeout) configuration option + +```json +{ + "browserInitTimeout": 180000 +} +``` + +* [runner.run Method](https://devexpress.github.io/testcafe/documentation/reference/testcafe-api/runner/run.html) parameter + +```js +runner.run({ "browserInitTimeout": 180000 }) +``` + +This setting sets an equal timeout for local and [remote browsers](https://devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#browsers-on-remote-devices). + +#### Improved `Unable To Establish Browser Connection` Error Message ([PR #5720](https://github.com/DevExpress/testcafe/pull/5720)) + +TestCafe raises this error when at least one local or remote browser was not able to connect. The error message now includes the number of browsers that have not established a connection. + +TestCafe raises a warning if low system performance is causing the connectivity issue. + +#### :gear: An Option to Retry Requests for the Test Page ([PR #5738](https://github.com/DevExpress/testcafe/pull/5738)) + +If a tested webpage was not served after the first request, TestCafe can now retry the request. + +You can enable this functionality with a command line, API, or configuration file option: + +* the [--retry-test-pages](https://devexpress.github.io/testcafe/documentation/using-testcafe/command-line-interface.html#--retry-test-pages) command line argument + + ```sh + testcafe chrome test.js --retry-test-pages + ``` + +* the [createTestCafe](https://devexpress.github.io/testcafe/documentation/reference/testcafe-api/global/createtestcafe.html) function parameter + + ```js + const createTestCafe = require('testcafe'); + + const testcafe = await createTestCafe('localhost', 1337, 1338, retryTestPages) + ``` + +* the [retryTestPages](https://devexpress.github.io/testcafe/documentation/using-testcafe/configuration-file.html#retrytestpages) configuration file property + + ```json + { + "retryTestPages": true + } + ``` + +### Bug Fixes + +* Fixed a bug where `Selector.withText` couldn't locate elements inside an `iframe` ([#5886](https://github.com/DevExpress/testcafe/issues/5886)) +* Fixed a bug where TestCafe was sometimes unable to detect when a browser instance closes ([#5857](https://github.com/DevExpress/testcafe/issues/5857)) +* You can now install TestCafe with `Yarn 2` ([PR #5872](https://github.com/DevExpress/testcafe/pull/5872) by [@NiavlysB](https://github.com/NiavlysB)) +* Fixed a bug where the `typeText` action does not always replace existing text ([PR #5942](https://github.com/DevExpress/testcafe/pull/5942) by [@rueyaa332266](https://github.com/rueyaa332266)) +* Fixed a bug where TestCafe was sometimes unable to create a `Web Worker` from an object ([testcafe-hammerhead/#2512](https://github.com/DevExpress/testcafe-hammerhead/issues/2512)) +* Fixed an error thrown by TestCafe proxy when trying to delete an object property that does not exist ([testcafe-hammerhead/#2504](https://github.com/DevExpress/testcafe-hammerhead/issues/2504)) +* Fixed an error thrown by TestCafe proxy when a Service Worker overwrites properties of a `window` object ([testcafe-hammerhead/#2538](https://github.com/DevExpress/testcafe-hammerhead/issues/2538)) +* Fixed a bug where `t.openWindow` method requested a URL twice ([testcafe-hammerhead/#2544](https://github.com/DevExpress/testcafe-hammerhead/issues/2544)) +* Fixed an error (`TypeError: Illegal invocation`) thrown by TestCafe on pages that contain an XMLDocument with an `iframe` ([testcafe-hammerhead/#2554](https://github.com/DevExpress/testcafe-hammerhead/issues/2554)) +* Fixed an error (`SyntaxError: Identifier has already been declared`) thrown by TestCafe on pages with scripts that create nested JavaScript objects ([testcafe-hammerhead/#2506](https://github.com/DevExpress/testcafe-hammerhead/issues/2506)) +* Fixed a bug where TestCafe was unable to focus elements within shadow DOM ([testcafe-hammerhead/#2408](https://github.com/DevExpress/testcafe-hammerhead/issues/2408)) +* TestCafe now throws an error when an entity of type other than `Error` is thrown in a test script ([PR testcafe-hammerhead/#2536](https://github.com/DevExpress/testcafe-hammerhead/pull/2536)) +* Fixed a bug where TestCafe was sometimes unable to resolve relative URLs ([testcafe-hammerhead/#2399](https://github.com/DevExpress/testcafe-hammerhead/issues/2399)) +* Properties of `window.location.constructor` are now shadowed correctly by TestCafe proxy ([testcafe-hammerhead/#2423](https://github.com/DevExpress/testcafe-hammerhead/issues/2423)) +* TestCafe proxy now correctly handles requests that are not permitted by the CORS policy ([testcafe-hammerhead/#1263](https://github.com/DevExpress/testcafe-hammerhead/issues/1263)) +* Improved compatibility with test pages that use `with` statements ([testcafe-hammerhead/#2434](https://github.com/DevExpress/testcafe-hammerhead/issues/2434)) +* TestCafe proxy can now properly parse statements that use a comma operator in `for..of` loops ([testcafe-hammerhead/#2573](https://github.com/DevExpress/testcafe-hammerhead/issues/2573)) +* Fixed a bug where TestCafe would open a new window even if `preventDefault` is present in element's event handler ([testcafe-hammerhead/#2582](https://github.com/DevExpress/testcafe-hammerhead/pull/2582)) + +### Vulnerability Fix ([PR #5843](https://github.com/DevExpress/testcafe/pull/5843), [PR testcafe-hammerhead#2531](https://github.com/DevExpress/testcafe-hammerhead/pull/2531)) + +We have fixed a vulnerability found in the [debug](https://www.npmjs.com/package/debug) module we use for debugging. +The vulnerability was a [ReDos Vulnerability Regression](https://github.com/visionmedia/debug/issues/797) that affected all TestCafe users. TestCafe now uses `debug@4.3.1`, where the issue is fixed. + +## v1.10.1 (2020-12-24) + +### Bug Fixes + +* Fixed an error thrown when TestCafe runs TypeScript tests ([#5808](https://github.com/DevExpress/testcafe/issues/5808)) +* Implemented a Service Worker that allows TestCafe to re-try failed requests to the tested page. This improves test stability ([#5239](https://github.com/DevExpress/testcafe/issues/5239)) +* Fixed an error thrown when you call the `t.getBrowserConsoleMessages` method ([#5600](https://github.com/DevExpress/testcafe/issues/5600)) + +## v1.10.0 (2020-12-15) + +### Enhancements + +#### Window Resize and Screenshot Support for Child Windows in Chrome ([PR #5661](https://github.com/DevExpress/testcafe/pull/5661), [PR #5567](https://github.com/DevExpress/testcafe/pull/5567)) + +You can now use the following actions in Google Chrome when you switch the test context to a [child window](https://devexpress.github.io/testcafe/documentation/guides/advanced-guides/multiple-browser-windows.html): + +* [t.maximizeWindow](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/maximize.html) +* [t.resizeWindow](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/resizewindow.html) +* [t.resizeWindowToFitDevice](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/resizewindowtofitdevice.html) +* [t.takeElementScreenshot](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/takeelementscreenshot.html) +* [t.takeScreenshot](https://devexpress.github.io/testcafe/documentation/reference/test-api/testcontroller/takescreenshot.html) + +#### New API to Specify Compiler Options ([#5519](https://github.com/DevExpress/testcafe/issues/5519)) + +In previous versions, you used the following methods to specify TypeScript compiler options: + +* the [--ts-config-path](https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--ts-config-path-path) command line flag + + ```sh + testcafe chrome my-tests --ts-config-path path/to/config.json + ``` + +* the [runner.tsConfigPath](https://devexpress.github.io/testcafe/documentation/reference/testcafe-api/runner/tsconfigpath.html) method + + ```js + runner.tsConfigPath('path/to/config.json'); + ``` + +* the [tsConfigPath](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#tsconfigpath) configuration file property + + ```json + { + "tsConfigPath": "path/to/config.json" + } + ``` + +In v1.10.0, we introduced a new easy-to-use API that allows you to specify the compiler options in the command line, API or TestCafe configuration file, without creating a separate JSON file. The new API is also designed to accept options for more compilers (for instance, Babel) in future releases. + +The API consists of the following members: + +* the [--compiler-options](https://devexpress.github.io/testcafe/documentation/reference/command-line-interface.html#--compiler-options-options) command line flag + + ```sh + testcafe chrome my-tests --compiler-options typescript.experimentalDecorators=true + ``` + +* the [runner.compilerOptions](https://devexpress.github.io/testcafe/documentation/reference/testcafe-api/runner/compileroptions.html) method + + ```js + runner.compilerOptions({ + typescript: { + experimentalDecorators: true + } + }); + ``` + +* the [compilerOptions](https://devexpress.github.io/testcafe/documentation/reference/configuration-file.html#compileroptions) configuration file property + + ```json + { + "compilerOptions": { + "typescript": { + "experimentalDecorators": true + } + } + } + ``` + +If you prefer to keep compiler settings in a configuration file, you can use the new API to specify the path to this file: + +```sh +testcafe chrome my-tests --compiler-options typescript.configPath='path/to/config.json' +``` + +In v1.10.0, you can customize TypeScript compiler options only. + +For more information, see [TypeScript and CoffeeScript](https://devexpress.github.io/testcafe/documentation/guides/concepts/typescript-and-coffeescript.html). + +#### Added a Selector Method to Access Shadow DOM ([PR #5560](https://github.com/DevExpress/testcafe/pull/5560) by [@mostlyfabulous](https://github.com/mostlyfabulous)) + +This release introduces the [selector.shadowRoot](https://devexpress.github.io/testcafe/documentation/reference/test-api/selector/shadowroot.html) method that allows you to access and interact with the shadow DOM elements. This method returns a shadow DOM root hosted in the selector's matched element. + +```js +import { Selector } from 'testcafe' + +fixture `Target Shadow DOM elements` + .page('https://devexpress.github.io/testcafe/example') + +test('Get text within shadow tree', async t => { + const shadowRoot = Selector('div').withAttribute('id', 'shadow-host').shadowRoot(); + const paragraph = shadowRoot.child('p'); + + await t.expect(paragraph.textContent).eql('This paragraph is in the shadow tree'); +}); +``` + +Note that you should chain other [selector methods](https://devexpress.github.io/testcafe/documentation/guides/basic-guides/select-page-elements.html#member-tables) to [selector.shadowRoot](https://devexpress.github.io/testcafe/documentation/reference/test-api/selector/shadowroot.html) to access elements in the shadow DOM. You cannot interact with the root element (an error occurs if you specify `selector.shadowRoot` as an action's target element). + +### Bug Fixes + +* Browsers now restart correctly on BrowserStack when the connection is lost ([#5238](https://github.com/DevExpress/testcafe/issues/5238)) +* Fixed an error that occurs if a child window is opened in an `iframe` ([#5033](https://github.com/DevExpress/testcafe/issues/5033)) +* TestCafe can now switch between the child and parent windows after the parent window is reloaded ([#5463](https://github.com/DevExpress/testcafe/issues/5463), [#5597](https://github.com/DevExpress/testcafe/issues/5597)) +* Fixed an issue when touch and mouse events fired on mobile devices even though the mouse event was prevented in page code ([#5380](https://github.com/DevExpress/testcafe/issues/5380)) +* Cross-domain `iframes` are now focused correctly in Safari ([#4793](https://github.com/DevExpress/testcafe/issues/4793)) +* Fixed an excessive warning displayed when an assertion is executed in a loop or against an element returned by a `selector.xxxSibling` method ([#5449](https://github.com/DevExpress/testcafe/issues/5449), [#5389](https://github.com/DevExpress/testcafe/issues/5389)) +* A page error is no longer emitted if the destination server responded with the `304` status ([#5025](https://github.com/DevExpress/testcafe/issues/5025)) +* Fixed an issue when TestCafe could not authenticate websites that use MSAL ([#4834](https://github.com/DevExpress/testcafe/issues/4834)) +* The `srcdoc` attributes for `iframes` are now processed ([testcafe-hammerhead/#1237](https://github.com/DevExpress/testcafe-hammerhead/issues/1237)) +* The `authorization` header is now preserved in response headers of fetch requests ([testcafe-hammerhead/#2334](https://github.com/DevExpress/testcafe-hammerhead/issues/2334)) +* The `document.title` for an `iframe` without `src` can now be correctly obtained in Firefox ([PR testcafe-hammerhead/#2466](https://github.com/DevExpress/testcafe-hammerhead/pull/2466)) +* TestCafe UI is now displayed correctly if the tested page's body content is added dynamically ([PR testcafe-hammerhead/#2454](https://github.com/DevExpress/testcafe-hammerhead/pull/2454)) +* Service Workers now receive `fetch` events ([testcafe-hammerhead/#2412](https://github.com/DevExpress/testcafe-hammerhead/issues/2412)) +* Fixed the case of headers sent to the web app server ([testcafe-hammerhead/#2344](https://github.com/DevExpress/testcafe-hammerhead/issues/2344)) +* `Location` objects in `iframes` without `src` now contain the correct data ([PR testcafe-hammerhead/#2448](https://github.com/DevExpress/testcafe-hammerhead/pull/2448)) +* Native function wrappers are now converted to strings correctly ([testcafe-hammerhead/#2394](https://github.com/DevExpress/testcafe-hammerhead/issues/2394)) +* Values retrieved from the local storage are now converted to strings ([testcafe-hammerhead/#2313](https://github.com/DevExpress/testcafe-hammerhead/issues/2313)) +* Fixed an issue when relative URLs were resolved incorrectly in `iframes` ([testcafe-hammerhead/#2461](https://github.com/DevExpress/testcafe-hammerhead/issues/2461)) +* Fixed an issue when TestCafe took a very long time to process large CSS files ([testcafe-hammerhead/#2475](https://github.com/DevExpress/testcafe-hammerhead/issues/2475)) +* Fixed an issue with client-side JavaScript processing ([testcafe-hammerhead/#2442](https://github.com/DevExpress/testcafe-hammerhead/issues/2442)) +* Fixed an issue that suppressed Adobe Launch Analytics requests ([testcafe-hammerhead/#2453](https://github.com/DevExpress/testcafe-hammerhead/issues/2453)) +* Added support for Web Workers created from Blob URLs ([testcafe-hammerhead/#1221](https://github.com/DevExpress/testcafe-hammerhead/issues/1221)) +* Fixed an issue when network requests were not received by the server ([testcafe-hammerhead/#2467](https://github.com/DevExpress/testcafe-hammerhead/issues/2467)) +* Cross-domain `iframe` source links now have the correct protocol when SSL is used ([PR testcafe-hammerhead/#2478](https://github.com/DevExpress/testcafe-hammerhead/pull/2478)) + +## v1.9.4 (2020-10-2) + +### Bug Fixes + +* Fixed an error thrown when TestCafe tested pages that access `document.title` ([#5559](https://github.com/DevExpress/testcafe/issues/5559), [PR testcafe-hammerhead/#2451](https://github.com/DevExpress/testcafe-hammerhead/pull/2451), [PR testcafe-hammerhead/#2446](https://github.com/DevExpress/testcafe-hammerhead/pull/2446)) +* Fixed a crash occurred when `null` was passed to the `createTestCafe()` API function ([#5549](https://github.com/DevExpress/testcafe/issues/5549)) +* Fixed an error thrown when the `content-encoding` header value was in the upper case ([testcafe-hammerhead/#2427](https://github.com/DevExpress/testcafe-hammerhead/issues/2427)) +* Fixed a crash that occurred in IE11 when Web Workers were used ([PR testcafe-hammerhead/#2441](https://github.com/DevExpress/testcafe-hammerhead/pull/2441) by [@danielroe](https://github.com/danielroe)) +* HTML nodes are now ordered correctly on tested web pages with Shadow UI ([PR testcafe-hammerhead/#2447](https://github.com/DevExpress/testcafe-hammerhead/pull/2447)) + +## v1.9.3 (2020-9-17) + +### Bug Fixes + +* Fixed the `RequestMock` type definitions to accept any type for the headers ([#5529](https://github.com/DevExpress/testcafe/issues/5529)) +* TestCafe no longer displays a warning about missing `await` when you save a snapshot property to a variable but do not use it later in the test ([#5534](https://github.com/DevExpress/testcafe/issues/5534)) +* Consecutive `document.getElementsByTagName('body')` calls now produce the correct results even if the first call was made before a document body was parsed entirely ([#5322](https://github.com/DevExpress/testcafe/issues/5322)) + +## v1.9.2 (2020-9-2) + +### Bug Fixes + +* TestCafe's TypeScript definitions now allow `null` as an expected value in assertions ([PR #5456](https://github.com/DevExpress/testcafe/pull/5456)) +* Added warnings displayed when a user tries to get a snapshot property value and misses `await` ([PR #5383](https://github.com/DevExpress/testcafe/pull/5383), part of [#5087](https://github.com/DevExpress/testcafe/issues/5087)) +* Tested app's standard output and error streams are forwarded to the TestCafe's debug log now ([#5423](https://github.com/DevExpress/testcafe/issues/5423)) +* Tested apps no longer cause buffer overflow errors when they output too much data to standard streams ([#2857](https://github.com/DevExpress/testcafe/issues/2857)) +* TestCafe now correctly waits for elements on tested pages that mock date functions ([#5447](https://github.com/DevExpress/testcafe/issues/5447)) +* HTTP header overflow errors now occur less frequently due to the increased maximum header size. Enhanced the error message with troubleshooting instructions ([testcafe-hammerhead/#2356](https://github.com/DevExpress/testcafe-hammerhead/issues/2356)) +* Added a descriptive message with troubleshooting instructions for the error thrown when the tested web server sends malformed or non-standard headers ([testcafe-hammerhead/#2188](https://github.com/DevExpress/testcafe-hammerhead/issues/2188)) +* Fixed a CSRF error on tested pages that use the `Request` class ([testcafe-hammerhead/#2140](https://github.com/DevExpress/testcafe-hammerhead/issues/2140)) +* `t.hover` now works correctly for tested pages built with the Styled Components framework ([#3830](https://github.com/DevExpress/testcafe/issues/3830)) +* Fixed script processing that could cause unhandled exceptions on some pages ([testcafe-hammerhead/#2417](https://github.com/DevExpress/testcafe-hammerhead/issues/2417)) +* Fixed the 'TypeError: r is not a function' uncaught exception on some pages with an `') + asyncTest('B237672 - TesCafe should not throw an exception "Access is denied" on accessing to a content of the cross-domain iframe', function () { + let result = false; + + const $iframe = $('') .width(500) .height(500) .attr('src', 'http://www.cross.domain.com') - .addClass(TEST_ELEMENT_CLASS) - .click(function () { - clicked = true; - }); + .addClass(TEST_ELEMENT_CLASS); window.QUnitGlobals.waitForIframe($iframe[0]).then(function () { try { - //NOTE: for not ie - var iframeBody = $iframe[0].contentWindow.document; + const iframeDocument = $iframe[0].contentWindow.document; - nativeMethods.addEventListener.call(iframeBody, 'click', function () { - clicked = true; + nativeMethods.addEventListener.call(iframeDocument, 'click', function () { + throw new Error('Click handler on an iframe should not be called'); }); + + result = true; } catch (e) { - // do nothing + result = false; } runClickAutomation($iframe[0], {}, function () { - ok(clicked, 'click was raised'); + ok(result); startNext(); }); }); @@ -367,19 +322,19 @@ $(document).ready(function () { }); asyncTest('B237862 - Test runner - the type action does not consider maxLength of the input element.', function () { - var initText = 'init'; - var newText = 'newnewnew'; - var $input = createInput().attr('value', initText); - var input = $input[0]; - var resultString = initText + newText; - var maxLength = 7; + const initText = 'init'; + const newText = 'newnewnew'; + const $input = createInput().attr('value', initText); + const input = $input[0]; + const resultString = initText + newText; + const maxLength = 7; $input.attr('maxLength', maxLength); equal(parseInt($input.attr('maxLength'), 10), 7); input.focus(); runTypeAutomation(input, newText, { - caretPos: input.value.length + caretPos: input.value.length, }) .then(function () { equal(input.value, resultString.substring(0, maxLength)); @@ -387,97 +342,96 @@ $(document).ready(function () { }); }); - if (!browserUtils.isIE) { - //TODO: IE wrong detection dimension top for element if this element have height more than scrollable container - //and element's top less than container top - asyncTest('B237890 - Wrong scroll before second click on big element in scrollable container', function () { - var clickCount = 0; - var errorScroll = false; - - var $scrollableContainer = $('
') - .css({ - position: 'absolute', - left: '200px', - top: '250px', - border: '1px solid black', - overflow: 'scroll' - }) - .width(250) - .height(200) - .addClass(TEST_ELEMENT_CLASS) - .appendTo(body); - - $('
').addClass(TEST_ELEMENT_CLASS) - .css({ - height: '20px', - width: '20px', - marginTop: 2350 + 'px', - backgroundColor: '#ffff00' - }) - .appendTo($scrollableContainer); - - $('
').addClass(TEST_ELEMENT_CLASS) - .css({ - position: 'absolute', - height: '20px', - width: '20px', - left: '600px' - }) - .appendTo(body); + // TODO: make the test stable on iOS + (browserUtils.isIOS ? QUnit.skip : asyncTest)('B237890 - Wrong scroll before second click on big element in scrollable container', function () { + let clickCount = 0; + let errorScroll = false; - var scrollHandler = function () { - if (clickCount === 1) - errorScroll = true; - }; + const $scrollableContainer = $('
') + .css({ + position: 'absolute', + left: '200px', + top: '250px', + border: '1px solid black', + overflow: 'scroll', + }) + .width(250) + .height(200) + .addClass(TEST_ELEMENT_CLASS) + .appendTo(body); - var bindScrollHandlers = function () { - $scrollableContainer.bind('scroll', scrollHandler); - $(window).bind('scroll', scrollHandler); - }; + $('
').addClass(TEST_ELEMENT_CLASS) + .css({ + height: '20px', + width: '20px', + marginTop: 2350 + 'px', + backgroundColor: '#ffff00', + }) + .appendTo($scrollableContainer); - var unbindScrollHandlers = function () { - $scrollableContainer.unbind('scroll', scrollHandler); - $(window).unbind('scroll', scrollHandler); - }; + $('
').addClass(TEST_ELEMENT_CLASS) + .css({ + position: 'absolute', + height: '20px', + width: '20px', + left: '600px', + }) + .appendTo(body); - var $element = $('
') - .addClass(TEST_ELEMENT_CLASS) - .css({ - width: '150px', - height: '400px', - position: 'absolute', - backgroundColor: 'red', - left: '50px', - top: '350px' - }) - .appendTo($scrollableContainer) - .bind('mousedown', function () { - unbindScrollHandlers(); - }) - .bind('click', function () { - clickCount++; + const scrollHandler = function () { + if (clickCount === 1) + errorScroll = true; + }; - }); + const bindScrollHandlers = function () { + $scrollableContainer.bind('scroll', scrollHandler); + $(window).bind('scroll', scrollHandler); + }; + + const unbindScrollHandlers = function () { + $scrollableContainer.unbind('scroll', scrollHandler); + $(window).unbind('scroll', scrollHandler); + }; + const $element = $('
') + .addClass(TEST_ELEMENT_CLASS) + .css({ + width: '150px', + height: '400px', + position: 'absolute', + backgroundColor: 'red', + left: '50px', + top: '350px', + }) + .appendTo($scrollableContainer) + .bind('mousedown', function () { + unbindScrollHandlers(); + }) + .bind('click', function () { + clickCount++; + + }); + + bindScrollHandlers(); + + runClickAutomation($element[0], {}, function () { + equal(clickCount, 1); bindScrollHandlers(); runClickAutomation($element[0], {}, function () { - equal(clickCount, 1); - bindScrollHandlers(); - - runClickAutomation($element[0], {}, function () { - equal(clickCount, 2); - ok(!errorScroll); - startNext(); - }); + equal(clickCount, 2); + ok(!errorScroll); + startNext(); }); }); - } + }); asyncTest('B237763 - ASPxPageControl - Lite render - Tabs are not clicked in Firefox', function () { - var clickRaised = false; - var $list = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var $b = $('').html('text').appendTo($list); + const $list = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $b = $('').html('text').appendTo($list); + + let clickRaised = false; + $list[0].onclick = function () { clickRaised = true; @@ -485,13 +439,13 @@ $(document).ready(function () { runClickAutomation($b[0], {}, function () { ok(clickRaised); - startNext(); + startNext(IS_MOBILE_SAFARI && 500); }); }); asyncTest('Click on label with for attribute', function () { - var $input = $('').addClass(TEST_ELEMENT_CLASS).attr('id', 'test123').appendTo('body'); - var $label = $('').addClass(TEST_ELEMENT_CLASS).attr('for', 'test123').appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).attr('id', 'test123').appendTo('body'); + const $label = $('').addClass(TEST_ELEMENT_CLASS).attr('for', 'test123').appendTo('body'); $input[0].checked = false; @@ -502,14 +456,15 @@ $(document).ready(function () { }); asyncTest('Q518957 - Test is inactive with mouse clicks and date-en-gb.js is included', function () { - var savedDateNow = window.Date; + const savedDateNow = window.Date; window.Date.now = function () { return {}; }; - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var completed = false; + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + + let completed = false; runHoverAutomation($input[0], function () { if (!completed) { @@ -530,9 +485,11 @@ $(document).ready(function () { }, 2000); }); - asyncTest('B238560 - Change event is not raised during TestCafe test running', function () { - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var changeRaised = false; + // TODO: stabilize test on iOS + (browserUtils.isIOS ? QUnit.skip : asyncTest)('B238560 - Change event is not raised during TestCafe test running', function () { + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + + let changeRaised = false; $input[0].addEventListener('change', function () { changeRaised = true; @@ -544,10 +501,12 @@ $(document).ready(function () { }); }); - asyncTest('B252929 - Wrong behavior during recording dblclick on input', function () { - var $input = createInput(); - var dblclickCount = 0; - var clickCount = 0; + // TODO: stabilize test on iOS + (browserUtils.isIOS ? QUnit.skip : asyncTest)('B252929 - Wrong behavior during recording dblclick on input', function () { + const $input = createInput(); + + let dblclickCount = 0; + let clickCount = 0; $input[0].value = 'Test cafe'; @@ -560,7 +519,7 @@ $(document).ready(function () { }); runDblClickAutomation($input[0], { - caretPos: 3 + caretPos: 3, }, function () { equal($input[0].selectionStart, 3, 'start selection correct'); equal($input[0].selectionEnd, 3, 'end selection correct'); @@ -571,8 +530,8 @@ $(document).ready(function () { }); asyncTest('B253465 - Incorrect behavior when a math function is typed in ASPxSpreadsheet\'s cell', function () { - var ROUND_BRACKET_KEY_CODE = 57; - var ROUND_BRACKET_CHAR_CODE = 40; + const ROUND_BRACKET_KEY_CODE = 57; + const ROUND_BRACKET_CHAR_CODE = 40; function checkKeyCode (e) { equal(e.keyCode, ROUND_BRACKET_KEY_CODE); @@ -582,25 +541,27 @@ $(document).ready(function () { equal(e.keyCode, ROUND_BRACKET_CHAR_CODE); } - var $input = createInput().keydown(checkKeyCode).keypress(checkCharCode).keyup(checkKeyCode); + const $input = createInput().keydown(checkKeyCode).keypress(checkCharCode).keyup(checkKeyCode); runTypeAutomation($input[0], '(', {}) .then(function () { startNext(); }); - expect(3); + const expectedAssertionCount = browserUtils.isAndroid ? 2 : 3; + + expect(expectedAssertionCount); }); asyncTest('B254340 - type in input with type="email"', function () { - var initText = 'support@devexpress.com'; - var newText = 'new'; - var $input = createInput('email').attr('value', initText); - var caretPos = 5; - var resultString = initText.substring(0, caretPos) + newText + initText.substring(caretPos); + const initText = 'support@devexpress.com'; + const newText = 'new'; + const $input = createInput('email').attr('value', initText); + const caretPos = 5; + const resultString = initText.substring(0, caretPos) + newText + initText.substring(caretPos); runTypeAutomation($input[0], newText, { - caretPos: caretPos + caretPos: caretPos, }) .then(function () { equal($input[0].value, resultString); @@ -610,116 +571,102 @@ $(document).ready(function () { }); }); - if (browserUtils.isIE) { - //TODO: fix it for other browsers - asyncTest('Unexpected focus events are raised during click', function () { - var input1FocusCount = 0; - var input2FocusCount = 0; - var $input1 = createInput().attr('id', '1').focus(function () { - input1FocusCount++; - }); - - var $input2 = createInput().attr('id', '2').focus(function () { - input2FocusCount++; - $input1[0].focus(); - }); - - runClickAutomation($input2[0], {}, function () { - equal(input1FocusCount, 1); - equal(input2FocusCount, 1); - - startNext(); - }); - }); - - asyncTest('Unexpected focus events are raised during dblclick', function () { - var input1FocusCount = 0; - var input2FocusCount = 0; - var $input1 = createInput().attr('id', '1').focus(function () { - input1FocusCount++; - }); - - var $input2 = createInput().attr('id', '2').focus(function () { - input2FocusCount++; - $input1[0].focus(); - }); - - runDblClickAutomation($input2[0], {}, function () { - equal(input1FocusCount, browserUtils.isIE ? 1 : 2); - equal(input2FocusCount, browserUtils.isIE ? 1 : 2); - - startNext(); - }); - }); - } - - if (browserUtils.isIE && browserUtils.version > 9) { - asyncTest('T109295 - User action act.click isn\'t raised by click on map', function () { - var initText = 'click'; - var $input = createInput('button').attr('value', initText).css({ - position: 'absolute', - left: '200px', - top: '200px' - }); + if (!browserUtils.isIOS && !browserUtils.isAndroid) { + asyncTest('GH-2325 - mouse events should have e.screenX and e.screenY properties', function () { + const promises = []; + const screenLeft = window.screenLeft || window.screenX; + const screenTop = window.screenTop || window.screenY; + const el = document.createElement('div'); + const mouseOutEl = document.createElement('div'); + + el.innerHTML = 'Click me'; + el.className = TEST_ELEMENT_CLASS; + mouseOutEl.innerHTML = 'Hover me'; + mouseOutEl.className = TEST_ELEMENT_CLASS; + + document.body.appendChild(el); + document.body.appendChild(mouseOutEl); + + const checkEventScreenXYOptions = function (eventName) { + let resolveFn; + + promises.push(new Promise(function (resolve) { + resolveFn = resolve; + })); + + const handler = function (e) { + ok(e.screenX > 0); + ok(e.screenY > 0); + equal(e.screenX, e.clientX + screenLeft); + equal(e.screenY, e.clientY + screenTop); + + resolveFn(); + el.removeEventListener(eventName, handler); + }; - var log = ''; - var listenedEvents = { - mouse: ['mouseover', 'mouseout', 'mousedown', 'mouseup', 'click'], - touch: ['touchstart', 'touchend'], - pointer: ['pointerover', 'pointerout', 'pointerdown', 'pointerup'], - MSevents: ['MSPointerOver', 'MSPointerOut', 'MSPointerDown', 'MSPointerUp'] + return handler; }; - var addListeners = function (el, events) { - $.each(events, function (index, event) { - el.addEventListener(event, function (e) { - if (log !== '') - log += ', '; + const addEventListener = function (eventName) { + el.addEventListener(eventName, checkEventScreenXYOptions(eventName)); + }; - log += e.type; + addEventListener('mousemove'); + addEventListener('mouseenter'); + addEventListener('mouseover'); + addEventListener('mousedown'); + addEventListener('mouseup'); + addEventListener('click'); + addEventListener('mouseout'); + addEventListener('mouseleave'); + addEventListener('contextmenu'); + addEventListener('dblclick'); + + const click = new ClickAutomation(el, { offsetX: 5, offsetY: 5 }, window, cursor); + const rClick = new RClickAutomation(el, { offsetX: 5, offsetY: 5 }); + const dblClick = new DblClickAutomation(el, { offsetX: 5, offsetY: 5 }); + const mouseOut = new ClickAutomation(mouseOutEl, { offsetX: 5, offsetY: 5 }, window, cursor); + + click.run() + .then(function () { + return mouseOut.run(); + }) + .then(function () { + return rClick.run(); + }) + .then(function () { + return dblClick.run(); + }) + .then(function () { + Promise.all(promises).then(function () { + startNext(); }); }); - }; - - addListeners($input[0], listenedEvents.mouse); - - if (browserUtils.version > 10) - addListeners($input[0], listenedEvents.pointer); - else - addListeners($input[0], listenedEvents.MSevents); - - runClickAutomation($input[0], {}, function () { - if (browserUtils.version > 10) - equal(log, 'pointerover, mouseover, pointerdown, mousedown, pointerup, mouseup, click'); - else - equal(log, 'MSPointerOver, mouseover, MSPointerDown, mousedown, MSPointerUp, mouseup, click'); - startNext(); - }); }); } asyncTest('T286582 - A menu item has a hover state in jssite tests, but it is not hovered', function () { - var style = [ + const style = [ '' + '', ].join('\n'); // NOTE: we need to use a sandboxed jQuery to process the 'style' element content. // Since Hammerhead 8.0.0, proxying is performed on prototypes (instead of elements) - var sandboxedJQuery = window.sandboxedJQuery.jQuery; + const sandboxedJQuery = window.sandboxedJQuery.jQuery; sandboxedJQuery(style) .addClass(TEST_ELEMENT_CLASS) .appendTo(body); - var $input1 = createInput() + const $input1 = createInput() .css('position', 'fixed') .css('margin-top', '50px') .appendTo(body); - var $input2 = $input1 + const $input2 = $input1 .clone() .css('margin-left', '200px') .appendTo(body); @@ -736,8 +683,8 @@ $(document).ready(function () { }); asyncTest('B254020 - act.type in input type="number" does not type sometimes to input on motorolla Xoom pad.', function () { - var newText = '123'; - var $input = createInput() + const newText = '123'; + const $input = createInput() .attr('placeholder', 'Type here...') .css('-webkit-user-modify', 'read-write-plaintext-only'); @@ -752,304 +699,305 @@ $(document).ready(function () { module('regression tests with input type="number"'); - if (!browserUtils.isIE9) { - asyncTest('B254340 - click on input with type="number"', function () { - var $input = createInput('number').val('123'); - var caretPos = 2; - var clickCount = 0; + asyncTest('B254340 - click on input with type="number"', function () { + const $input = createInput('number').val('123'); + const caretPos = 2; - $input.click(function () { - clickCount++; - }); + let clickCount = 0; - runClickAutomation($input[0], { - caretPos: caretPos - }, function () { - equal(textSelection.getSelectionStart($input[0]), caretPos, 'start selection correct'); - equal(textSelection.getSelectionEnd($input[0]), caretPos, 'end selection correct'); - equal(clickCount, 1); - startNext(); - }); + $input.click(function () { + clickCount++; }); - if (!browserUtils.isFirefox) { - asyncTest('B254340 - select in input with type="number"', function () { - var initText = '12345678987654321'; - var input = createInput('number').attr('value', initText).val(initText)[0]; - var startPos = 5; - var endPos = 11; - var backward = true; + runClickAutomation($input[0], { + caretPos: caretPos, + }, function () { + equal(textSelection.getSelectionStart($input[0]), caretPos, 'start selection correct'); + equal(textSelection.getSelectionEnd($input[0]), caretPos, 'end selection correct'); + equal(clickCount, 1); + startNext(); + }); + }); - var selectTextAutomation = new SelectTextAutomation(input, endPos, startPos, {}); + if (!browserUtils.isFirefox) { + asyncTest('B254340 - select in input with type="number"', function () { + const initText = '12345678987654321'; + const input = createInput('number').attr('value', initText).val(initText)[0]; + const startPos = 5; + const endPos = 11; + const backward = true; - selectTextAutomation - .run() - .then(function () { - equal(textSelection.getSelectionStart(input), startPos, 'start selection correct'); - equal(textSelection.getSelectionEnd(input), endPos, 'end selection correct'); - equal(textSelection.hasInverseSelection(input), backward, 'selection direction correct'); + const selectTextAutomation = new SelectTextAutomation(input, endPos, startPos, {}); - startNext(); - }); - }); - } - - asyncTest('T133144 - Incorrect typing into an input with type "number" in FF during test executing (without caretPos)', function () { - var initText = '12345'; - var text = '123'; - var newText = initText + text; - var $input = createInput('number').attr('value', initText); - - runTypeAutomation($input[0], text, {}) + selectTextAutomation + .run() .then(function () { - equal($input[0].value, newText); - equal(textSelection.getSelectionStart($input[0]), newText.length, 'start selection correct'); - equal(textSelection.getSelectionEnd($input[0]), newText.length, 'end selection correct'); + equal(textSelection.getSelectionStart(input), startPos, 'start selection correct'); + equal(textSelection.getSelectionEnd(input), endPos, 'end selection correct'); + equal(textSelection.hasInverseSelection(input), backward, 'selection direction correct'); startNext(); }); }); + } - asyncTest('T133144 - Incorrect typing into an input with type "number" in FF during test executing (with caretPos)', function () { - var initText = '12345'; - var text = '123'; - var $input = createInput('number').attr('value', initText); - var caretPos = 2; + asyncTest('T133144 - Incorrect typing into an input with type "number" in FF during test executing (without caretPos)', function () { + const initText = '12345'; + const text = '123'; + const newText = initText + text; + const $input = createInput('number').attr('value', initText); - runTypeAutomation($input[0], text, { - caretPos: caretPos - }) - .then(function () { - equal($input[0].value, initText.substring(0, caretPos) + text + initText.substring(caretPos)); - equal(textSelection.getSelectionStart($input[0]), caretPos + - text.length, 'start selection correct'); + runTypeAutomation($input[0], text, {}) + .then(function () { + equal($input[0].value, newText); + equal(textSelection.getSelectionStart($input[0]), newText.length, 'start selection correct'); + equal(textSelection.getSelectionEnd($input[0]), newText.length, 'end selection correct'); - equal(textSelection.getSelectionEnd($input[0]), caretPos + text.length, 'end selection correct'); + startNext(); + }); + }); - startNext(); - }); - }); + asyncTest('T133144 - Incorrect typing into an input with type "number" in FF during test executing (with caretPos)', function () { + const initText = '12345'; + const text = '123'; + const $input = createInput('number').attr('value', initText); + const caretPos = 2; - asyncTest('T133144 - Incorrect typing into an input with type "number" in FF during test executing (with replace)', function () { - var initText = '12345'; - var text = '678'; - var $input = createInput('number').attr('value', initText); + runTypeAutomation($input[0], text, { + caretPos: caretPos, + }) + .then(function () { + equal($input[0].value, initText.substring(0, caretPos) + text + initText.substring(caretPos)); + equal(textSelection.getSelectionStart($input[0]), caretPos + + text.length, 'start selection correct'); - runTypeAutomation($input[0], text, { - replace: true - }) - .then(function () { - equal($input[0].value, text); - equal(textSelection.getSelectionStart($input[0]), text.length, 'start selection correct'); - equal(textSelection.getSelectionEnd($input[0]), text.length, 'end selection correct'); + equal(textSelection.getSelectionEnd($input[0]), caretPos + text.length, 'end selection correct'); - startNext(); - }); - }); + startNext(); + }); + }); - asyncTest('T138385 - input type="number" leave out "maxlength" attribute (act.type)', function () { - var $input = createInput('number').attr('maxLength', 2); - var inputEventCount = 0; + asyncTest('T133144 - Incorrect typing into an input with type "number" in FF during test executing (with replace)', function () { + const initText = '12345'; + const text = '678'; + const $input = createInput('number').attr('value', initText); - $input.bind('input', function () { - inputEventCount++; + runTypeAutomation($input[0], text, { + replace: true, + }) + .then(function () { + equal($input[0].value, text); + equal(textSelection.getSelectionStart($input[0]), text.length, 'start selection correct'); + equal(textSelection.getSelectionEnd($input[0]), text.length, 'end selection correct'); + + startNext(); }); + }); - runTypeAutomation($input[0], '123', {}) - .then(function () { - equal(inputEventCount, 3); - equal($input.val(), browserUtils.isIE ? '12' : '123'); + asyncTest('T138385 - input type="number" leave out "maxlength" attribute (act.type)', function () { + const $input = createInput('number').attr('maxLength', 2); - startNext(); - }); + let inputEventCount = 0; + + $input.bind('input', function () { + inputEventCount++; }); - asyncTest('T138385 - input type "number" leave out "maxlength" attribute (act.press)', function () { - var $input = createInput('number').attr('maxLength', 2); - var inputEventCount = 0; - var keySequence = '1 2 3'; - var pressAutomation = new PressAutomation(parseKeySequence(keySequence).combinations, {}); + runTypeAutomation($input[0], '123', {}) + .then(function () { + equal(inputEventCount, 3); + equal($input.val(), '123'); - $input.bind('input', function () { - inputEventCount++; + startNext(); }); + }); - $input[0].focus(); + asyncTest('T138385 - input type "number" leave out "maxlength" attribute (act.press)', function () { + const $input = createInput('number').attr('maxLength', 2); + const keySequence = '1 2 3'; + const pressAutomation = new PressAutomation(parseKeySequence(keySequence).combinations, {}); - pressAutomation - .run() - .then(function () { - equal(inputEventCount, 3); - equal($input.val(), browserUtils.isIE ? '12' : '123'); + let inputEventCount = 0; - startNext(); - }); + $input.bind('input', function () { + inputEventCount++; }); - asyncTest('B254340 - type letters in input with type="number" (symbol in start)', function () { - var input = createInput('number')[0]; + $input[0].focus(); - runTypeAutomation(input, '+12', {}) - .then(function () { - equal(input.value, '12'); - input.value = ''; + pressAutomation + .run() + .then(function () { + equal(inputEventCount, 3); + equal($input.val(), '123'); - return runTypeAutomation(input, '-12', {}); - }) - .then(function () { - equal(input.value, '-12'); - input.value = ''; + startNext(); + }); + }); - return runTypeAutomation(input, '.12', {}); - }) - .then(function () { - equal(input.value, '.12'); - input.value = ''; + asyncTest('B254340 - type letters in input with type="number" (symbol in start)', function () { + const input = createInput('number')[0]; - return runTypeAutomation(input, '+-12', {}); - }) - .then(function () { - equal(input.value, '-12'); - input.value = ''; + runTypeAutomation(input, '+12', {}) + .then(function () { + equal(input.value, '12'); + input.value = ''; - return runTypeAutomation(input, 'a12', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + return runTypeAutomation(input, '-12', {}); + }) + .then(function () { + equal(input.value, '-12'); + input.value = ''; - return runTypeAutomation(input, '$12', {}); - }) - .then(function () { - equal(input.value, '12'); + return runTypeAutomation(input, '.12', {}); + }) + .then(function () { + equal(input.value, '.12'); + input.value = ''; - startNext(); - }); - }); + return runTypeAutomation(input, '+-12', {}); + }) + .then(function () { + equal(input.value, '-12'); + input.value = ''; - asyncTest('B254340 - type letters in input with type="number" (symbol in the middle)', function () { - var input = createInput('number')[0]; + return runTypeAutomation(input, 'a12', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; - runTypeAutomation(input, '1+2', {}) - .then(function () { - equal(input.value, '12'); - input.value = ''; + return runTypeAutomation(input, '$12', {}); + }) + .then(function () { + equal(input.value, '12'); - return runTypeAutomation(input, '1-2', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + startNext(); + }); + }); - return runTypeAutomation(input, '1.2', {}); - }) - .then(function () { - equal(input.value, '1.2'); - input.value = ''; + asyncTest('B254340 - type letters in input with type="number" (symbol in the middle)', function () { + const input = createInput('number')[0]; - return runTypeAutomation(input, '1+-2', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + runTypeAutomation(input, '1+2', {}) + .then(function () { + equal(input.value, '12'); + input.value = ''; - return runTypeAutomation(input, '1a2', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + return runTypeAutomation(input, '1-2', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; - return runTypeAutomation(input, '1$2', {}); - }) - .then(function () { - equal(input.value, '12'); - document.body.removeChild(input); - startNext(); - }); - }); + return runTypeAutomation(input, '1.2', {}); + }) + .then(function () { + equal(input.value, '1.2'); + input.value = ''; - asyncTest('B254340 - type letters in input with type="number" (symbol in the end)', function () { - var input = createInput('number')[0]; + return runTypeAutomation(input, '1+-2', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; - runTypeAutomation(input, '12+', {}) - .then(function () { - equal(input.value, '12'); - input.value = ''; + return runTypeAutomation(input, '1a2', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; - return runTypeAutomation(input, '12-', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + return runTypeAutomation(input, '1$2', {}); + }) + .then(function () { + equal(input.value, '12'); + document.body.removeChild(input); + startNext(); + }); + }); - return runTypeAutomation(input, '12.', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + asyncTest('B254340 - type letters in input with type="number" (symbol in the end)', function () { + const input = createInput('number')[0]; - return runTypeAutomation(input, '12+-', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + runTypeAutomation(input, '12+', {}) + .then(function () { + equal(input.value, '12'); + input.value = ''; - return runTypeAutomation(input, '12a', {}); - }) - .then(function () { - equal(input.value, '12'); - input.value = ''; + return runTypeAutomation(input, '12-', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; - return runTypeAutomation(input, '12$', {}); - }) - .then(function () { - equal(input.value, '12'); - document.body.removeChild(input); + return runTypeAutomation(input, '12.', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; - startNext(); - }); - }); + return runTypeAutomation(input, '12+-', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; + + return runTypeAutomation(input, '12a', {}); + }) + .then(function () { + equal(input.value, '12'); + input.value = ''; - asyncTest('B254340 - type letters in input with type="number" (one symbol)', function () { - var input = createInput('number').val('12')[0]; + return runTypeAutomation(input, '12$', {}); + }) + .then(function () { + equal(input.value, '12'); + document.body.removeChild(input); - runTypeAutomation(input, '+', { caretPos: 0 }) - .then(function () { - equal(input.value, '12'); - input.value = '12'; + startNext(); + }); + }); - return runTypeAutomation(input, '-', { caretPos: 0 }); - }) - .then(function () { - equal(input.value, '-12'); - input.value = '12'; + asyncTest('B254340 - type letters in input with type="number" (one symbol)', function () { + const input = createInput('number').val('12')[0]; - return runTypeAutomation(input, '.', { caretPos: 0 }); - }) - .then(function () { - equal(input.value, '.12'); - input.value = '12'; + runTypeAutomation(input, '+', { caretPos: 0 }) + .then(function () { + equal(input.value, '12'); + input.value = '12'; - return runTypeAutomation(input, '+-', { caretPos: 0 }); - }) - .then(function () { - equal(input.value, '-12'); - input.value = '12'; + return runTypeAutomation(input, '-', { caretPos: 0 }); + }) + .then(function () { + equal(input.value, '-12'); + input.value = '12'; - return runTypeAutomation(input, '$', { caretPos: 0 }); - }) - .then(function () { - equal(input.value, '12'); - input.value = '12'; + return runTypeAutomation(input, '.', { caretPos: 0 }); + }) + .then(function () { + equal(input.value, '.12'); + input.value = '12'; - return runTypeAutomation(input, '-12', { caretPos: 0 }); - }) - .then(function () { - equal(input.value, '-1212'); - document.body.removeChild(input); + return runTypeAutomation(input, '+-', { caretPos: 0 }); + }) + .then(function () { + equal(input.value, '-12'); + input.value = '12'; - startNext(); - }); - }); - } + return runTypeAutomation(input, '$', { caretPos: 0 }); + }) + .then(function () { + equal(input.value, '12'); + input.value = '12'; + + return runTypeAutomation(input, '-12', { caretPos: 0 }); + }) + .then(function () { + equal(input.value, '-1212'); + document.body.removeChild(input); + + startNext(); + }); + }); }); diff --git a/test/client/fixtures/automation/select-element-test.js b/test/client/fixtures/automation/select-element-test.js index 0c150e89304..403c61e18ab 100644 --- a/test/client/fixtures/automation/select-element-test.js +++ b/test/client/fixtures/automation/select-element-test.js @@ -1,48 +1,53 @@ -var hammerhead = window.getTestCafeModule('hammerhead'); -var browserUtils = hammerhead.utils.browser; -var featureDetection = hammerhead.utils.featureDetection; -var shadowUI = hammerhead.shadowUI; +const hammerhead = window.getTestCafeModule('hammerhead'); +const browserUtils = hammerhead.utils.browser; +const featureDetection = hammerhead.utils.featureDetection; +const shadowUI = hammerhead.shadowUI; +const Promise = hammerhead.Promise; -var testCafeCore = window.getTestCafeModule('testCafeCore'); -var parseKeySequence = testCafeCore.get('./utils/parse-key-sequence'); +const testCafeCore = window.getTestCafeModule('testCafeCore'); +const parseKeySequence = testCafeCore.parseKeySequence; +const selectController = testCafeCore.selectController; testCafeCore.preventRealEvents(); -var testCafeAutomation = window.getTestCafeModule('testCafeAutomation'); -var ClickOptions = testCafeAutomation.get('../../test-run/commands/options').ClickOptions; -var PressAutomation = testCafeAutomation.Press; -var DblClickAutomation = testCafeAutomation.DblClick; -var ClickAutomation = testCafeAutomation.Click; -var getOffsetOptions = testCafeAutomation.getOffsetOptions; +const testCafeAutomation = window.getTestCafeModule('testCafeAutomation'); +const ClickOptions = testCafeAutomation.ClickOptions; +const PressAutomation = testCafeAutomation.Press; +const DblClickAutomation = testCafeAutomation.DblClick; +const ClickAutomation = testCafeAutomation.Click; +const SelectChildClickAutomation = testCafeAutomation.SelectChildClick; +const getOffsetOptions = testCafeAutomation.getOffsetOptions; +const cursor = testCafeAutomation.cursor; -var testCafeUI = window.getTestCafeModule('testCafeUI'); -var selectElement = testCafeUI.get('./select-element'); +const isMobileSafari = browserUtils.isSafari && featureDetection.isTouchDevice; +const nextTestDelay = 200; $(document).ready(function () { //consts - var TEST_ELEMENT_CLASS = 'testElement'; - var OPTION_CLASS = 'tcOption'; - var OPTION_GROUP_CLASS = 'tcOptionGroup'; - var OPTION_LIST_CLASS = 'tcOptionList'; + const TEST_ELEMENT_CLASS = 'testElement'; + const OPTION_CLASS = 'tcOption'; + const OPTION_GROUP_CLASS = 'tcOptionGroup'; + const OPTION_LIST_CLASS = 'tcOptionList'; //utils - var handlersLog = []; - var isMobileBrowser = featureDetection.isTouchDevice; + const isMobileBrowser = featureDetection.isTouchDevice; - var createOption = function (parent, text) { + let handlersLog = []; + + const createOption = function (parent, text) { return $('').text(text) .addClass(TEST_ELEMENT_CLASS) .appendTo(parent); }; - var createOptionGroup = function (select, label) { + const createOptionGroup = function (select, label) { return $('').attr('label', label) .addClass(TEST_ELEMENT_CLASS) .appendTo(select)[0]; }; - var createSelect = function (size) { - var select = $('') + const createSelect = function (size) { + const select = $('') .addClass(TEST_ELEMENT_CLASS) .appendTo('body')[0]; @@ -58,24 +63,24 @@ $(document).ready(function () { return select; }; - var createSelectWithGroups = function () { - var select = $('') + const createSelectWithGroups = function () { + const select = $('') .addClass(TEST_ELEMENT_CLASS) .appendTo('body')[0]; - var firstGroup = createOptionGroup(select, 'First'); + const firstGroup = createOptionGroup(select, 'First'); createOption(firstGroup, 'one'); createOption(firstGroup, 'two'); createOption(firstGroup, 'three'); - var secondGroup = createOptionGroup(select, 'Second'); + const secondGroup = createOptionGroup(select, 'Second'); createOption(secondGroup, 'four'); createOption(secondGroup, 'five'); createOption(secondGroup, 'six'); - var thirdGroup = createOptionGroup(select, 'Third'); + const thirdGroup = createOptionGroup(select, 'Third'); createOption(thirdGroup, 'sevent'); createOption(thirdGroup, 'eight'); @@ -83,20 +88,20 @@ $(document).ready(function () { return select; }; - var createSelectWithGroupsForCheckPress = function () { - var select = $('') + const createSelectWithGroupsForCheckPress = function () { + const select = $('') .addClass(TEST_ELEMENT_CLASS) .appendTo('body')[0]; - var firstGroup = createOptionGroup(select, 'Group 1'); + const firstGroup = createOptionGroup(select, 'Group 1'); createOption(firstGroup, 'one'); - var secondGroup = createOptionGroup(select, 'Group 2'); + const secondGroup = createOptionGroup(select, 'Group 2'); createOption(secondGroup, 'two'); - var thirdGroup = createOptionGroup(select, 'Group 3'); + const thirdGroup = createOptionGroup(select, 'Group 3'); createOption(thirdGroup, 'thee'); $(thirdGroup).attr('disabled', 'disabled'); @@ -106,17 +111,18 @@ $(document).ready(function () { return select; }; - var runPressAutomation = function (keys, callback) { - var pressAutomation = new PressAutomation(parseKeySequence(keys).combinations, {}); + const runPressAutomation = function (keys, callback) { + const pressAutomation = new PressAutomation(parseKeySequence(keys).combinations, {}); pressAutomation .run() .then(callback); }; - var runDblClickAutomation = function (el, options, callback) { - var clickOptions = new ClickOptions(); - var offsets = getOffsetOptions(el, options.offsetX, options.offsetY); + const runDblClickAutomation = function (el, options, callback) { + const offsets = getOffsetOptions(el, options.offsetX, options.offsetY); + + const clickOptions = new ClickOptions(); clickOptions.offsetX = offsets.offsetX; clickOptions.offsetY = offsets.offsetY; @@ -126,18 +132,18 @@ $(document).ready(function () { ctrl: options.ctrl, alt: options.ctrl, shift: options.shift, - meta: options.meta + meta: options.meta, }; - var dblClickAutomation = new DblClickAutomation(el, clickOptions); + const dblClickAutomation = new DblClickAutomation(el, clickOptions); - dblClickAutomation + return dblClickAutomation .run() .then(callback); }; - var preventDefault = function (e) { - var ev = e || window.event; + const preventDefault = function (e) { + const ev = e || window.event; if (ev.preventDefault) ev.preventDefault(); @@ -145,12 +151,12 @@ $(document).ready(function () { ev.returnValue = false; }; - var eventHandler = function (e) { + const eventHandler = function (e) { if (e.target === this) handlersLog.push(e.target.tagName.toLowerCase() + ' ' + e.type); }; - var bindSelectAndOptionHandlers = function ($select, $option) { + const bindSelectAndOptionHandlers = function ($select, $option) { $select.bind('mousedown', eventHandler); $select.bind('mouseup', eventHandler); $select.bind('click', eventHandler); @@ -166,20 +172,20 @@ $(document).ready(function () { $('body').css('height', 1500); - var startNext = function () { - if (browserUtils.isIE) { + const startNext = function () { + if (isMobileSafari) { removeTestElements(); - window.setTimeout(start, 30); + window.setTimeout(start, nextTestDelay); } else start(); }; - var removeTestElements = function () { + const removeTestElements = function () { $('.' + TEST_ELEMENT_CLASS).remove(); }; - var pressDownUpKeysActions = function (select) { + const pressDownUpKeysActions = function (select) { window.async.series({ firstPressDownAction: function (callback) { equal(select.selectedIndex, 0); @@ -251,11 +257,11 @@ $(document).ready(function () { equal(select.selectedIndex, 0); startNext(); }); - } + }, }); }; - var pressRightLeftKeysActions = function (select) { + const pressRightLeftKeysActions = function (select) { window.async.series({ firstPressRightAction: function (callback) { equal(select.selectedIndex, 0); @@ -327,11 +333,11 @@ $(document).ready(function () { equal(select.selectedIndex, 0); startNext(); }); - } + }, }); }; - var pressDownUpKeysActionsForSelectWithOptgroups = function (select, testCallback) { + const pressDownUpKeysActionsForSelectWithOptgroups = function (select, testCallback) { window.async.series({ pressDownFirstTime: function (callback) { equal(select.selectedIndex, 0); @@ -361,47 +367,48 @@ $(document).ready(function () { equal(select.selectedIndex, 0); testCallback(); }); - } + }, }); }; - var pressRightLeftKeysActionsForSelectWithOptgroups = function ($select, testCallback, notChangeInChrome) { + const pressRightLeftKeysActionsForSelectWithOptgroups = function ($select, testCallback, notChangeInChrome) { window.async.series({ pressDownFirstTime: function (callback) { equal($select.selectedIndex, 0); runPressAutomation('right', function () { - equal($select.selectedIndex, browserUtils.isIE || notChangeInChrome ? 0 : 1); + equal($select.selectedIndex, notChangeInChrome ? 0 : 1); callback(); }); }, pressDownSecondTime: function (callback) { runPressAutomation('right', function () { - equal($select.selectedIndex, browserUtils.isIE || notChangeInChrome ? 0 : 3); + equal($select.selectedIndex, notChangeInChrome ? 0 : 3); callback(); }); }, pressUpFirstTime: function (callback) { runPressAutomation('left', function () { - equal($select.selectedIndex, browserUtils.isIE || notChangeInChrome ? 0 : 1); + equal($select.selectedIndex, notChangeInChrome ? 0 : 1); callback(); }); }, pressUpSecondTime: function () { runPressAutomation('up', function () { - equal($select.selectedIndex, browserUtils.isIE || notChangeInChrome ? 0 : 0); + equal($select.selectedIndex, notChangeInChrome ? 0 : 0); testCallback(); }); - } + }, }); }; - var runClickAutomation = function (el, options, callback) { - var clickOptions = new ClickOptions(); - var offsets = getOffsetOptions(el, options.offsetX, options.offsetY); + const runClickAutomation = function (el, options, callback) { + const offsets = getOffsetOptions(el, options.offsetX, options.offsetY); + + const clickOptions = new ClickOptions(); clickOptions.offsetX = offsets.offsetX; clickOptions.offsetY = offsets.offsetY; @@ -411,27 +418,37 @@ $(document).ready(function () { ctrl: options.ctrl, alt: options.ctrl, shift: options.shift, - meta: options.meta + meta: options.meta, }; - var clickAutomation = new ClickAutomation(el, clickOptions); + const clickAutomation = /opt/i.test(el.tagName) ? + new SelectChildClickAutomation(el, clickOptions) : + new ClickAutomation(el, clickOptions, window, cursor); - clickAutomation + return clickAutomation .run() - .then(callback); + .then(function () { + callback(); + }); + }; + + const createOnFocusPromise = function (el) { + return new Promise(function (resolve) { + el.addEventListener('focus', resolve); + }); }; QUnit.testDone(function () { - if (!browserUtils.isIE) - removeTestElements(); + removeTestElements(); handlersLog = []; }); //tests module('common tests'); + asyncTest('option list doesn\'t open if mousedown event prevented', function () { - var select = createSelect(); + const select = createSelect(); select['onmousedown'] = preventDefault; @@ -444,15 +461,18 @@ $(document).ready(function () { }); module('mouse actions with select element'); + asyncTest('click on select and on option', function () { - var select = createSelect(); - var option = $(select).children()[2]; + const select = createSelect(); + const option = $(select).children()[2]; runClickAutomation(select, {}, function () { equal(select.selectedIndex, 0); runClickAutomation(option, {}, function () { + equal(select.selectedIndex, 2); + window.setTimeout(function () { startNext(); }, 0); @@ -461,46 +481,56 @@ $(document).ready(function () { }); module('press actions with select element'); + asyncTest('press down/up/right/left when option list closed', function () { - var select = createSelect(); + const select = createSelect(); - $(select).focus(); + const selectOnFocusPromise = createOnFocusPromise(select); - window.async.series({ - pressDownAction: function (callback) { - equal(select.selectedIndex, 0); + select.focus(); - runPressAutomation('down', function () { - equal(select.selectedIndex, 1); - callback(); - }); - }, + selectOnFocusPromise + .then(function () { + window.async.series({ + pressDownAction: function (callback) { + equal(select.selectedIndex, 0); - pressUpAction: function (callback) { - runPressAutomation('up', function () { - equal(select.selectedIndex, 0); - callback(); - }); - }, + runPressAutomation('down', function () { + equal(select.selectedIndex, 1); - pressRightAction: function (callback) { - runPressAutomation('right', function () { - equal(select.selectedIndex, browserUtils.isIE ? 0 : 1); - callback(); - }); - }, + callback(); + }); + }, - pressLeftAction: function () { - runPressAutomation('left', function () { - equal(select.selectedIndex, 0); - startNext(); + pressUpAction: function (callback) { + runPressAutomation('up', function () { + equal(select.selectedIndex, 0); + + callback(); + }); + }, + + pressRightAction: function (callback) { + runPressAutomation('right', function () { + equal(select.selectedIndex, 1); + + callback(); + }); + }, + + pressLeftAction: function () { + runPressAutomation('left', function () { + equal(select.selectedIndex, 0); + + startNext(); + }); + }, }); - } - }); + }); }); asyncTest('press down/up/right/left when option list opened', function () { - var select = createSelect(); + const select = createSelect(); window.async.series({ openSelectList: function (callback) { @@ -545,12 +575,12 @@ $(document).ready(function () { runClickAutomation(select, {}, function () { window.setTimeout(startNext, 0); }); - } + }, }); }); asyncTest('click select and press enter', function () { - var select = createSelect(); + const select = createSelect(); window.async.series({ 'Click on select': function (callback) { @@ -574,12 +604,12 @@ $(document).ready(function () { equal($(shadowUI.select('.' + OPTION_LIST_CLASS)).is(':visible'), false); equal(select.selectedIndex, 0); startNext(); - } + }, }); }); asyncTest('click select and press tab', function () { - var select = createSelect(); + const select = createSelect(); window.async.series({ 'Click on select': function (callback) { @@ -599,12 +629,12 @@ $(document).ready(function () { equal($(shadowUI.select('.' + OPTION_LIST_CLASS)).is(':visible'), false); equal(select.selectedIndex, 0); startNext(); - } + }, }); }); asyncTest('click select and press esc', function () { - var select = createSelect(); + const select = createSelect(); window.async.series({ 'Click on select': function (callback) { @@ -624,7 +654,7 @@ $(document).ready(function () { equal($(shadowUI.select('.' + OPTION_LIST_CLASS)).is(':visible'), false); equal(select.selectedIndex, 0); startNext(); - } + }, }); }); @@ -632,21 +662,21 @@ $(document).ready(function () { if (isMobileBrowser) { module('mouse actions with multiline select element'); asyncTest('click on the "select" element with the "size" attribute greater than one, then click on an option', function () { - var select = createSelect(2); + const select = createSelect(2); runClickAutomation(select, {}, function () { - ok(selectElement.isOptionListExpanded(select)); + ok(selectController.isOptionListExpanded(select)); startNext(); }); }); asyncTest('click on the "select" element with the "multiple" attribute, then click on an option', function () { - var select = createSelect(); + const select = createSelect(); $(select).attr('multiple', 'multiple'); runClickAutomation(select, {}, function () { - ok(selectElement.isOptionListExpanded(select)); + ok(selectController.isOptionListExpanded(select)); startNext(); }); }); @@ -654,7 +684,7 @@ $(document).ready(function () { else { module('mouse actions with select element with attribute size more than one'); asyncTest('click on option', function () { - var select = createSelect(2); + const select = createSelect(2); select.selectedIndex = 0; equal(select.selectedIndex, 0); @@ -666,7 +696,7 @@ $(document).ready(function () { }); asyncTest('click on option with scroll', function () { - var select = createSelect(2); + const select = createSelect(2); select.selectedIndex = 0; equal(select.selectedIndex, 0); @@ -678,8 +708,8 @@ $(document).ready(function () { }); asyncTest('click on disabled option', function () { - var select = createSelect(2); - var $option = $(select).children(':first'); + const select = createSelect(2); + const $option = $(select).children(':first'); $option.attr('disabled', 'disabled'); @@ -693,7 +723,7 @@ $(document).ready(function () { }); asyncTest('click on an option (the select size is more than the option count)', function () { - var select = createSelect(10); + const select = createSelect(10); select.selectedIndex = 0; equal(select.selectedIndex, 0); @@ -705,54 +735,72 @@ $(document).ready(function () { }); module('press actions with select element with attribute size more than one'); + asyncTest('press down/up in select', function () { - var select = createSelect(); + const select = createSelect(); $(select).attr('size', '2'); - //NOTE: IE11 sets selectedIndex = -1 after setting attribute size != 1 - select.selectedIndex = 0; - $(select).focus(); - pressDownUpKeysActions(select); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressDownUpKeysActions(select); + }); }); asyncTest('press right/left in select', function () { - var select = createSelect(); + const select = createSelect(); $(select).attr('size', '2'); - //NOTE: IE11 sets selectedIndex = -1 after setting attribute size != 1 - select.selectedIndex = 0; - $(select).focus(); - pressRightLeftKeysActions(select); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressRightLeftKeysActions(select); + }); }); asyncTest('press down/up in select with "size" more than the option count', function () { - var select = createSelect(); + const select = createSelect(); $(select).attr('size', '10'); - //NOTE: IE11 sets selectedIndex = -1 after setting attribute size != 1 - select.selectedIndex = 0; - $(select).focus(); - pressDownUpKeysActions(select); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressDownUpKeysActions(select); + }); }); asyncTest('press right/left in select with "size" more than the option count', function () { - var select = createSelect(); + const select = createSelect(); $(select).attr('size', '10'); - //NOTE: IE11 sets selectedIndex = -1 after setting attribute size != 1 - select.selectedIndex = 0; - $(select).focus(); - pressRightLeftKeysActions(select); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressRightLeftKeysActions(select); + }); }); module('mouse actions with the "select" element with the "multiple" attribute'); + asyncTest('click on option with scroll', function () { - var select = createSelect(); + const select = createSelect(); select.selectedIndex = 0; equal(select.selectedIndex, 0); @@ -765,7 +813,7 @@ $(document).ready(function () { }); asyncTest('click on option with scroll (attribute size more than one)', function () { - var select = createSelect(2); + const select = createSelect(2); select.selectedIndex = 0; equal(select.selectedIndex, 0); @@ -778,7 +826,7 @@ $(document).ready(function () { }); asyncTest('click on option with scroll (attribute size less than one)', function () { - var select = createSelect(-1); + const select = createSelect(-1); select.selectedIndex = 0; equal(select.selectedIndex, 0); @@ -793,9 +841,10 @@ $(document).ready(function () { module('mouse actions with select with option groups'); asyncTest('click select and option', function () { - var select = createSelectWithGroups(); - var option = $(select).find('option')[3]; - var changeHandled = false; + const select = createSelectWithGroups(); + const option = $(select).find('option')[3]; + + let changeHandled = false; //T280587 - Selecting an option does not trigger the on('change', ...) event select.onchange = function () { @@ -818,9 +867,9 @@ $(document).ready(function () { }); asyncTest('click select and option (optgroup with empty label)', function () { - var select = createSelectWithGroups(); - var $optgroup = $(select).find('optgroup').eq(0).attr('label', ''); - var option = $optgroup.find('option')[1]; + const select = createSelectWithGroups(); + const $optgroup = $(select).find('optgroup').eq(0).attr('label', ''); + const option = $optgroup.find('option')[1]; runClickAutomation(select, {}, function () { equal(select.selectedIndex, 0); @@ -837,9 +886,9 @@ $(document).ready(function () { }); asyncTest('click select and option (in disabled group)', function () { - var select = createSelectWithGroups(); - var $optgroup = $(select).find('optgroup').eq(1).attr('disabled', ''); - var option = $optgroup.find('option')[0]; + const select = createSelectWithGroups(); + const $optgroup = $(select).find('optgroup').eq(1).attr('disabled', ''); + const option = $optgroup.find('option')[0]; runClickAutomation(select, {}, function () { equal(select.selectedIndex, 0); @@ -860,8 +909,8 @@ $(document).ready(function () { }); asyncTest('click select and optgroup', function () { - var select = createSelectWithGroups(); - var group = $(select).find('optgroup')[2]; + const select = createSelectWithGroups(); + const group = $(select).find('optgroup')[2]; runClickAutomation(select, {}, function () { equal(select.selectedIndex, 0); @@ -883,13 +932,13 @@ $(document).ready(function () { }); asyncTest('click select and option in subgroup', function () { - var select = createSelectWithGroups(); - var $optgroup = $(select).find('optgroup').eq(1); - var $newOptgroup = $('') + const select = createSelectWithGroups(); + const $optgroup = $(select).find('optgroup').eq(1); + const $newOptgroup = $('') .addClass(TEST_ELEMENT_CLASS) .appendTo($optgroup[0]); - var $newOption = $('').text('sub option') + const $newOption = $('').text('sub option') .addClass(TEST_ELEMENT_CLASS) .appendTo($newOptgroup[0]); @@ -908,9 +957,9 @@ $(document).ready(function () { }); asyncTest('click select and subgroup', function () { - var select = createSelectWithGroups(); - var $optgroup = $(select).find('optgroup').eq(1); - var $newOptgroup = $('') + const select = createSelectWithGroups(); + const $optgroup = $(select).find('optgroup').eq(1); + const $newOptgroup = $('') .addClass(TEST_ELEMENT_CLASS) .appendTo($optgroup[0]); @@ -938,9 +987,9 @@ $(document).ready(function () { }); asyncTest('click select and option out of group', function () { - var select = createSelectWithGroups(); - var $optgroup = $(select).find('optgroup').eq(1); - var $newOption = $('').text('outer option') + const select = createSelectWithGroups(); + const $optgroup = $(select).find('optgroup').eq(1); + const $newOption = $('').text('outer option') .addClass(TEST_ELEMENT_CLASS) .insertAfter($optgroup); @@ -958,57 +1007,76 @@ $(document).ready(function () { }); }); - module('press actions with select with option groups'); + asyncTest('press down/up when option list closed', function () { - var select = createSelectWithGroupsForCheckPress(); + const select = createSelectWithGroupsForCheckPress(); select.selectedIndex = 0; - $(select).focus(); - pressDownUpKeysActionsForSelectWithOptgroups(select, startNext); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressDownUpKeysActionsForSelectWithOptgroups(select, startNext); + }); }); asyncTest('press right/left when option list closed', function () { - var select = createSelectWithGroupsForCheckPress(); + const select = createSelectWithGroupsForCheckPress(); select.selectedIndex = 0; - $(select).focus(); - pressRightLeftKeysActionsForSelectWithOptgroups(select, startNext); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressRightLeftKeysActionsForSelectWithOptgroups(select, startNext); + }); }); asyncTest('press down/up when option list opened', function () { - var select = createSelectWithGroupsForCheckPress(); + const select = createSelectWithGroupsForCheckPress(); select.selectedIndex = 0; - $(select).focus(); - window.async.series({ - openSelectList: function (callback) { - equal(select.selectedIndex, 0); + const selectOnFocusPromise = createOnFocusPromise(select); - runClickAutomation(select, {}, function () { - //NOTE: we should wait for binding handlers to the document - equal(select.selectedIndex, 0); - window.setTimeout(callback, 0); - }); - }, + select.focus(); - pressDownUpKeysActionsForSelectWithOptgroups: function (callback) { - pressDownUpKeysActionsForSelectWithOptgroups(select, callback); - }, + selectOnFocusPromise + .then(function () { + window.async.series({ + openSelectList: function (callback) { + equal(select.selectedIndex, 0); - closeSelectList: function () { - runClickAutomation(select, {}, function () { - window.setTimeout(startNext, 0); + runClickAutomation(select, {}, function () { + //NOTE: we should wait for binding handlers to the document + equal(select.selectedIndex, 0); + + window.setTimeout(callback, 0); + }); + }, + + pressDownUpKeysActionsForSelectWithOptgroups: function (callback) { + pressDownUpKeysActionsForSelectWithOptgroups(select, callback); + }, + + closeSelectList: function () { + runClickAutomation(select, {}, function () { + window.setTimeout(startNext, 0); + }); + }, }); - } - }); + }); }); asyncTest('press right/left when option list opened', function () { - var select = createSelectWithGroupsForCheckPress(); + const select = createSelectWithGroupsForCheckPress(); window.async.series({ openSelectList: function (callback) { @@ -1029,7 +1097,7 @@ $(document).ready(function () { runClickAutomation(select, {}, function () { window.setTimeout(startNext, 0); }); - } + }, }); }); @@ -1037,15 +1105,15 @@ $(document).ready(function () { if (!isMobileBrowser) { module('mouse actions with select with the "groups" option and size more than one'); asyncTest('click optgroup', function () { - var select = createSelectWithGroups(); - var $select = $(select); - var group = $select.find('optgroup')[0]; + const select = createSelectWithGroups(); + const $select = $(select); + const group = $select.find('optgroup')[0]; $select.attr('size', '4'); select.selectedIndex = 0; window.setTimeout(function () { - //NOTE: when setting the selected option, IE and Mozilla scroll the select + //NOTE: when setting the selected option, Mozilla scroll the select $select.scrollTop(0); runClickAutomation(group, {}, function () { @@ -1058,20 +1126,20 @@ $(document).ready(function () { }); asyncTest('click option', function () { - var select = createSelectWithGroups(); - var $select = $(select); - var option = $select.find('option')[1]; + const select = createSelectWithGroups(); + const $select = $(select); + const option = $select.find('option')[1]; $select.css({ position: 'absolute', top: '200px', - left: '300px' + left: '300px', }); $select.attr('size', '5'); select.selectedIndex = 0; window.setTimeout(function () { - //NOTE: when setting the selected option, IE and Mozilla scroll the select + //NOTE: when setting the selected option, Mozilla scroll the select $select.scrollTop(0); runClickAutomation(option, {}, function () { @@ -1085,19 +1153,19 @@ $(document).ready(function () { }); asyncTest('click option with scroll down', function () { - var select = createSelectWithGroups(); - var $select = $(select); - var option = $select.find('option')[8]; - var selectElementScroll = 0; + const select = createSelectWithGroups(); + const $select = $(select); + const option = $select.find('option')[8]; + const selectElementScroll = 0; $select.css({ position: 'absolute', top: '200px', - left: '300px' + left: '300px', }); $select.attr('size', '5'); select.selectedIndex = 0; - //NOTE: when setting the selected option, IE and Mozilla scroll the select + //NOTE: when setting the selected option, Mozilla scroll the select $select.scrollTop(selectElementScroll); window.setTimeout(function () { @@ -1114,15 +1182,15 @@ $(document).ready(function () { }); asyncTest('click option with scroll up', function () { - var select = createSelectWithGroups(); - var $select = $(select); - var option = $select.find('option')[4]; - var selectElementScroll = 119; + const select = createSelectWithGroups(); + const $select = $(select); + const option = $select.find('option')[4]; + const selectElementScroll = 119; $select.css({ position: 'absolute', top: '200px', - left: '300px' + left: '300px', }); $select.attr('size', '5'); select.selectedIndex = 8; @@ -1142,27 +1210,27 @@ $(document).ready(function () { }); asyncTest('click option in subgroup', function () { - var select = createSelectWithGroups(); - var $select = $(select); - var $optgroup = $select.find('optgroup').eq(1); - var $newOptgroup = $('') + const select = createSelectWithGroups(); + const $select = $(select); + const $optgroup = $select.find('optgroup').eq(1); + const $newOptgroup = $('') .addClass(TEST_ELEMENT_CLASS) .appendTo($optgroup[0]); - var $newOption = $('').text('sub option') + const $newOption = $('').text('sub option') .addClass(TEST_ELEMENT_CLASS) .appendTo($newOptgroup[0]); $select.css({ position: 'absolute', top: '200px', - left: '300px' + left: '300px', }); $select.attr('size', '5'); select.selectedIndex = 0; window.setTimeout(function () { - //NOTE: when setting the selected option, IE and Mozilla scroll the select + //NOTE: when setting the selected option, Mozilla scroll the select $select.scrollTop(0); runClickAutomation($newOption[0], {}, function () { @@ -1176,10 +1244,10 @@ $(document).ready(function () { }); asyncTest('click subgroup', function () { - var select = createSelectWithGroups(); - var $select = $(select); - var $optgroup = $select.find('optgroup').eq(1); - var $newOptgroup = $('') + const select = createSelectWithGroups(); + const $select = $(select); + const $optgroup = $select.find('optgroup').eq(1); + const $newOptgroup = $('') .addClass(TEST_ELEMENT_CLASS) .appendTo($optgroup[0]); @@ -1190,13 +1258,13 @@ $(document).ready(function () { $select.css({ position: 'absolute', top: '200px', - left: '300px' + left: '300px', }); $select.attr('size', '5'); select.selectedIndex = 0; window.setTimeout(function () { - //NOTE: when setting the selected option, IE and Mozilla scroll the select + //NOTE: when setting the selected option, Mozilla scroll the select $select.scrollTop(0); runClickAutomation($newOptgroup[0], {}, function () { @@ -1210,10 +1278,10 @@ $(document).ready(function () { }); asyncTest('click option out of group', function () { - var select = createSelectWithGroups(); - var $select = $(select); - var $optgroup = $select.find('optgroup').eq(2); - var $newOption = $('').text('outer option') + const select = createSelectWithGroups(); + const $select = $(select); + const $optgroup = $select.find('optgroup').eq(2); + const $newOption = $('').text('outer option') .addClass(TEST_ELEMENT_CLASS) .insertAfter($optgroup); @@ -1221,7 +1289,7 @@ $(document).ready(function () { select.selectedIndex = 0; window.setTimeout(function () { - //NOTE: when setting the selected option, IE and Mozilla scroll the select + //NOTE: when setting the selected option, Mozilla scroll the select $select.scrollTop(0); runClickAutomation($newOption[0], {}, function () { @@ -1237,29 +1305,41 @@ $(document).ready(function () { module('press actions with select with the "groups" option and size more than one'); asyncTest('press down/up', function () { - var select = createSelectWithGroupsForCheckPress(); + const select = createSelectWithGroupsForCheckPress(); $(select).attr('size', '5'); select.selectedIndex = 0; - $(select).focus(); - pressDownUpKeysActionsForSelectWithOptgroups(select, startNext); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressDownUpKeysActionsForSelectWithOptgroups(select, startNext); + }); }); asyncTest('press right/left', function () { - var select = createSelectWithGroupsForCheckPress(); + const select = createSelectWithGroupsForCheckPress(); $(select).attr('size', '5'); select.selectedIndex = 0; - $(select).focus(); - pressRightLeftKeysActionsForSelectWithOptgroups(select, startNext, browserUtils.isWebKit); + const selectOnFocusPromise = createOnFocusPromise(select); + + select.focus(); + + selectOnFocusPromise + .then(function () { + pressRightLeftKeysActionsForSelectWithOptgroups(select, startNext, browserUtils.isWebKit); + }); }); } module('regression'); asyncTest('B237794 - Select options list doesn\'t close after dblclick in Chrome and Opera (dblclick)', function () { - var select = createSelect(); + const select = createSelect(); equal(shadowUI.select('.' + OPTION_LIST_CLASS).length, 0); @@ -1268,26 +1348,13 @@ $(document).ready(function () { notEqual($(shadowUI.select('.' + OPTION_LIST_CLASS)).is(':visible'), true); equal(shadowUI.select('.' + OPTION_LIST_CLASS).length, 0); equal(select.selectedIndex, 0); - startNext(); - }); - }); - - asyncTest('B237794 - Select options list doesn\'t close after dblclick in Chrome and Opera (dblclick)', function () { - var select = createSelect(); - equal(shadowUI.select('.' + OPTION_LIST_CLASS).length, 0); - - runDblClickAutomation(select, {}, function () { - equal(select, document.activeElement); - notEqual($(shadowUI.select('.' + OPTION_LIST_CLASS)).is(':visible'), true); - equal(shadowUI.select('.' + OPTION_LIST_CLASS).length, 0); - equal(select.selectedIndex, 0); startNext(); }); }); asyncTest('B237794 - Select options list doesn\'t close after dblclick in Chrome and Opera (two successive clicks)', function () { - var select = createSelect(); + const select = createSelect(); equal(shadowUI.select('.' + OPTION_LIST_CLASS).length, 0); @@ -1315,14 +1382,14 @@ $(document).ready(function () { equal(shadowUI.select('.' + OPTION_LIST_CLASS).length, 0); equal(select.selectedIndex, 0); startNext(); - } + }, }); }); asyncTest('B238984 - After click on select element and then click on another first opened options list doesn\'t closed', function () { - var firstSelect = createSelect(); - var secondSelect = createSelect(); - var $secondSelectOption = $(secondSelect).find('option').last(); + const firstSelect = createSelect(); + const secondSelect = createSelect(); + const $secondSelectOption = $(secondSelect).find('option').last(); equal(shadowUI.select('.' + OPTION_LIST_CLASS).length, 0); @@ -1364,13 +1431,13 @@ $(document).ready(function () { window.setTimeout(function () { startNext(); }, 0); - } + }, }); }); asyncTest('B253370 - Event handlers are called in the wrong order (click on select and option)', function () { - var select = createSelect(); - var $option = $(select).children().eq(2); + const select = createSelect(); + const $option = $(select).children().eq(2); //bind handlers bindSelectAndOptionHandlers($(select), $option); @@ -1383,8 +1450,6 @@ $(document).ready(function () { equal(select.selectedIndex, 2); if (browserUtils.isFirefox) equal(handlersLog.join(), 'select mousedown,select mouseup,select click,option mousedown,option mouseup,select input,select change,option click'); - else if (browserUtils.isIE) - equal(handlersLog.join(), 'select mousedown,select mouseup,select click,select mousedown,select mouseup,select change,option click'); else if (isMobileBrowser) equal(handlersLog.join(), 'select mousedown,select mouseup,select click,select input,select change'); else @@ -1395,8 +1460,8 @@ $(document).ready(function () { }); asyncTest('B253370 - Event handlers are called in the wrong order (click on select and the same option)', function () { - var select = createSelect(); - var $option = $(select).children().eq(2); + const select = createSelect(); + const $option = $(select).children().eq(2); //bind handlers bindSelectAndOptionHandlers($(select), $option); @@ -1413,8 +1478,6 @@ $(document).ready(function () { if (browserUtils.isFirefox) equal(handlersLog.join(), 'select mousedown,select mouseup,select click,option mousedown,option mouseup,select input,select change,option click,select mousedown,select mouseup,select click,option mousedown,option mouseup,option click'); - else if (browserUtils.isIE) - equal(handlersLog.join(), 'select mousedown,select mouseup,select click,select mousedown,select mouseup,select change,option click,select mousedown,select mouseup,select click,select mousedown,select mouseup,option click'); else if (isMobileBrowser) equal(handlersLog.join(), 'select mousedown,select mouseup,select click,select input,select change,select mousedown,select mouseup,select click'); else @@ -1427,8 +1490,8 @@ $(document).ready(function () { }); asyncTest('B253370 - Event handlers are called in the wrong order (click on select and select)', function () { - var select = createSelect(); - var $option = $(select).children().eq(2); + const select = createSelect(); + const $option = $(select).children().eq(2); //bind handlers bindSelectAndOptionHandlers($(select), $option); @@ -1446,8 +1509,8 @@ $(document).ready(function () { }); asyncTest('B253370 - Event handlers are called in the wrong order (click on option in select with size more than one)', function () { - var select = createSelect(4); - var $option = $(select).children().eq(2); + const select = createSelect(4); + const $option = $(select).children().eq(2); //bind handlers bindSelectAndOptionHandlers($(select), $option); @@ -1467,21 +1530,19 @@ $(document).ready(function () { 'Click on option': function () { runClickAutomation($option[0], {}, function () { equal(select.selectedIndex, 2); - if (browserUtils.isIE) - equal(handlersLog.join(), 'select mousedown,select mouseup,select change,select click'); - else if (isMobileBrowser) + if (isMobileBrowser) equal(handlersLog.join(), 'select mousedown,select mouseup,select click,select input,select change'); else equal(handlersLog.join(), 'option mousedown,option mouseup,select input,select change,option click'); startNext(); }); - } + }, }); }); asyncTest('B253370 - Event handlers are called in the wrong order (click on the same option in select with size more than one)', function () { - var select = createSelect(4); - var $option = $(select).children().eq(2); + const select = createSelect(4); + const $option = $(select).children().eq(2); //bind handlers bindSelectAndOptionHandlers($(select), $option); @@ -1512,20 +1573,18 @@ $(document).ready(function () { 'Click on option second time': function () { runClickAutomation($option[0], {}, function () { equal(select.selectedIndex, 2); - if (browserUtils.isIE) - equal(handlersLog.join(), 'select mousedown,select mouseup,select change,select click,select mousedown,select mouseup,select click'); - else if (isMobileBrowser) + if (isMobileBrowser) equal(handlersLog.join(), 'select mousedown,select mouseup,select click,select input,select change,select mousedown,select mouseup,select click'); else equal(handlersLog.join(), 'option mousedown,option mouseup,select input,select change,option click,option mousedown,option mouseup,option click'); startNext(); }); - } + }, }); }); asyncTest('B253370 - Event handlers are called in the wrong order (click on select with size more than one)', function () { - var select = createSelect(30); + const select = createSelect(30); //bind handlers bindSelectAndOptionHandlers($(select)); @@ -1537,18 +1596,4 @@ $(document).ready(function () { startNext(); }); }); - - asyncTest('GH234 - Value of a ').addClass(TEST_ELEMENT_CLASS).appendTo($form); + const $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); @@ -61,8 +61,8 @@ $(document).ready(function () { }); asyncTest('submit button (button type="submit")', function () { - var $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); + const $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); @@ -81,8 +81,8 @@ $(document).ready(function () { }); asyncTest('without submit button', function () { - var $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); + const $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); @@ -90,8 +90,8 @@ $(document).ready(function () { }); asyncTest('not-submit button (button type="button")', function () { - var $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); + const $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); @@ -100,8 +100,8 @@ $(document).ready(function () { }); asyncTest('disabled submit button', function () { - var $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); + const $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); @@ -110,8 +110,8 @@ $(document).ready(function () { }); asyncTest('inputs types "text" and "search" and without submit button', function () { - var $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); + const $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); @@ -119,9 +119,9 @@ $(document).ready(function () { }); asyncTest('valid and invalid text input ("text" and "url") and submit button', function () { - var $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); - var $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); - var $urlInput = $('').addClass(TEST_ELEMENT_CLASS).val('test').appendTo($form); + const $form = $('
').addClass(TEST_ELEMENT_CLASS).appendTo('body'); + const $input = $('').addClass(TEST_ELEMENT_CLASS).appendTo($form); + const $urlInput = $('').addClass(TEST_ELEMENT_CLASS).val('test').appendTo($form); $('
+ +
+
+ Element with text +
Element with text
+
Element with text
+
Element with text
+
Element with text
1
+
Element with text1
+
+
+ eh
@@ -125,5 +137,31 @@
+ +
+
displayNone
+
zeroDimensions
+
visibilityHidden
+
visible
+
+ +
+
+ + +

3

+

4

+

5

+
+
+

1

+

2

+

3

+ +

5

+

6

+
+
+ diff --git a/test/functional/fixtures/api/es-next/selector/pages/shadow.html b/test/functional/fixtures/api/es-next/selector/pages/shadow.html new file mode 100644 index 00000000000..fa5d84f627d --- /dev/null +++ b/test/functional/fixtures/api/es-next/selector/pages/shadow.html @@ -0,0 +1,48 @@ + + + + + Title + + + + + + + diff --git a/test/functional/fixtures/api/es-next/selector/pages/visible.html b/test/functional/fixtures/api/es-next/selector/pages/visible.html new file mode 100644 index 00000000000..9a6ac2e0945 --- /dev/null +++ b/test/functional/fixtures/api/es-next/selector/pages/visible.html @@ -0,0 +1,11 @@ + + + + + Title + + +
+
text
+ + diff --git a/test/functional/fixtures/api/es-next/selector/test.js b/test/functional/fixtures/api/es-next/selector/test.js index b693da297b7..b3fd0809bdd 100644 --- a/test/functional/fixtures/api/es-next/selector/test.js +++ b/test/functional/fixtures/api/es-next/selector/test.js @@ -1,8 +1,8 @@ -var expect = require('chai').expect; +const expect = require('chai').expect; -var DEFAULT_SELECTOR_TIMEOUT = 3000; -var DEFAULT_RUN_OPTIONS = { selectorTimeout: DEFAULT_SELECTOR_TIMEOUT }; -var DEFAULT_CHROME_RUN_OPTIONS = { only: 'chrome', selectorTimeout: 3000 }; +const DEFAULT_SELECTOR_TIMEOUT = 3000; +const DEFAULT_RUN_OPTIONS = { selectorTimeout: DEFAULT_SELECTOR_TIMEOUT }; +const DEFAULT_CHROME_RUN_OPTIONS = { only: 'chrome', selectorTimeout: DEFAULT_SELECTOR_TIMEOUT }; describe('[API] Selector', function () { it('Should provide basic properties in HTMLElement snapshots', function () { @@ -61,6 +61,10 @@ describe('[API] Selector', function () { return runTests('./testcafe-fixtures/selector-test.js', 'Selector `addCustomMethods` method', DEFAULT_RUN_OPTIONS); }); + it('Selector `addCustomMethods` method - Selector mode', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Selector `addCustomMethods` method - Selector mode', DEFAULT_RUN_OPTIONS); + }); + it('Should wait for element to appear on new page', function () { return runTests('./testcafe-fixtures/selector-test.js', 'Element on new page', DEFAULT_RUN_OPTIONS); }); @@ -77,6 +81,10 @@ describe('[API] Selector', function () { return runTests('./testcafe-fixtures/selector-test.js', 'Selector "withText" method', DEFAULT_CHROME_RUN_OPTIONS); }); + it('Should filter results with `withExactText()` method', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Selector "withExactText" method', DEFAULT_CHROME_RUN_OPTIONS); + }); + it('Should filter results with `withAttribute()` method', function () { return runTests('./testcafe-fixtures/selector-test.js', 'Selector "withAttribute" method', DEFAULT_CHROME_RUN_OPTIONS); }); @@ -89,6 +97,14 @@ describe('[API] Selector', function () { return runTests('./testcafe-fixtures/selector-test.js', 'Combination of filter methods', DEFAULT_CHROME_RUN_OPTIONS); }); + it('Should provide methods for filtering by visibility for plain structure of HTML elements', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Selector `filterVisible/filterHidden` methods with plain structure', DEFAULT_RUN_OPTIONS); + }); + + it('Should provide methods for filtering by visibility for hierarchical structure of HTML elements', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Selector `filterVisible/filterHidden` methods with hierarchical structure', DEFAULT_RUN_OPTIONS); + }); + it('Should provide .find() method', function () { return runTests('./testcafe-fixtures/selector-test.js', 'Selector "find" method', DEFAULT_RUN_OPTIONS); }); @@ -105,8 +121,30 @@ describe('[API] Selector', function () { return runTests('./testcafe-fixtures/selector-test.js', 'Selector "sibling" method', DEFAULT_RUN_OPTIONS); }); - it('Should provide .nextSibling() method', function () { - return runTests('./testcafe-fixtures/selector-test.js', 'Selector "nextSibling" method', DEFAULT_RUN_OPTIONS); + it('Selector "shadowRoot" method - children are found', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Selector "shadowRoot" method - children are found', Object.assign({ skip: ['edge'] }, DEFAULT_RUN_OPTIONS)); + }); + + it('Selector "shadowRoot" method - shadow root not found', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Selector "shadowRoot" method - shadow root not found', Object.assign({ shouldFail: true, skip: ['edge'] }, DEFAULT_RUN_OPTIONS)) + .catch(function (errs) { + expect(errs[0]).contains('The specified selector does not match any element in the DOM tree.'); + expect(errs[0]).contains('| Selector(\'p\')'); + expect(errs[0]).contains('> | .shadowRoot()'); + expect(errs[0]).contains('| .find(\'div\')'); + }); + }); + + it('Selector "shadowRoot" method - content property', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Selector "shadowRoot" method - content property', Object.assign({ skip: ['edge'] }, DEFAULT_RUN_OPTIONS)); + }); + + it('Cannot use "shadowRoot" as a target', function () { + return runTests('./testcafe-fixtures/selector-test.js', 'Cannot use "shadowRoot" as a target', Object.assign({ shouldFail: true, skip: ['edge'] }, DEFAULT_RUN_OPTIONS)) + .catch(function (errs) { + expect(errs[0]).contains('The specified selector is expected to match a DOM element, but it matches a document fragment node.'); + expect(/> \d* |\s{4}await t.click(shadowRoot);/.test(errs[0])).to.be.ok; + }); }); it('Should provide .prevSibling() method', function () { @@ -129,6 +167,10 @@ describe('[API] Selector', function () { return runTests('./testcafe-fixtures/selector-test.js', 'hasAttribute method', DEFAULT_RUN_OPTIONS); }); + it('Should not fail on accessing "visible" property for a non-existing element (GH-2386)', () => { + return runTests('./testcafe-fixtures/selector-visible-test.js', null, DEFAULT_RUN_OPTIONS); + }); + describe('Errors', function () { it('Should handle errors in Selector code', function () { return runTests('./testcafe-fixtures/selector-error-test.js', 'Error in code', { shouldFail: true }) @@ -153,11 +195,12 @@ describe('[API] Selector', function () { it('Should raise an error if Selector ctor argument is not a function or string', function () { return runTests('./testcafe-fixtures/selector-error-test.js', 'Selector fn is not a function or string', { shouldFail: true, - only: 'chrome' + only: 'chrome', }).catch(function (errs) { expect(errs[0].indexOf( - 'Selector is expected to be initialized with a function, CSS selector string, another Selector, ' + - 'node snapshot or a Promise returned by a Selector, but number was passed.' + 'Cannot initialize a Selector because Selector is number, ' + + 'and not one of the following: a CSS selector string, a Selector object, a node snapshot, ' + + 'a function, or a Promise returned by a Selector.' )).eql(0); expect(errs[0]).contains('> 19 | await Selector(123)();'); @@ -167,11 +210,12 @@ describe('[API] Selector', function () { it("Should raise error if snapshot property shorthand can't find element in DOM tree", function () { return runTests('./testcafe-fixtures/selector-error-test.js', "Snapshot property shorthand - selector doesn't match any element", { shouldFail: true, - only: 'chrome' + only: 'chrome', }) .catch(function (errs) { expect(errs[0]).contains( - 'Cannot obtain information about the node because the specified selector does not match any node in the DOM tree.' + 'Cannot obtain information about the node because the specified selector does not match any node in the DOM tree.' + + ' > | Selector(\'#someUnknownElement\')' ); expect(errs[0]).contains("> 23 | await Selector('#someUnknownElement').tagName;"); }); @@ -180,11 +224,12 @@ describe('[API] Selector', function () { it("Should raise error if snapshot shorthand method can't find element in DOM tree", function () { return runTests('./testcafe-fixtures/selector-error-test.js', "Snapshot shorthand method - selector doesn't match any element", { shouldFail: true, - only: 'chrome' + only: 'chrome', }) .catch(function (errs) { expect(errs[0]).contains( - 'Cannot obtain information about the node because the specified selector does not match any node in the DOM tree.' + 'Cannot obtain information about the node because the specified selector does not match any node in the DOM tree.' + + ' > | Selector(\'#someUnknownElement\')' ); expect(errs[0]).contains("> 27 | await Selector('#someUnknownElement').getStyleProperty('width');"); }); @@ -193,7 +238,7 @@ describe('[API] Selector', function () { it('Should raise error if error occurs in selector during shorthand property evaluation', function () { return runTests('./testcafe-fixtures/selector-error-test.js', 'Snapshot property shorthand - selector error', { shouldFail: true, - only: 'chrome' + only: 'chrome', }) .catch(function (errs) { expect(errs[0]).contains( @@ -206,7 +251,7 @@ describe('[API] Selector', function () { it('Should raise error if error occurs in selector during shorthand method evaluation', function () { return runTests('./testcafe-fixtures/selector-error-test.js', 'Snapshot shorthand method - selector error', { shouldFail: true, - only: 'chrome' + only: 'chrome', }) .catch(function (errs) { expect(errs[0]).contains( @@ -219,7 +264,7 @@ describe('[API] Selector', function () { it('Should raise error if error occurs in selector during "count" property evaluation', function () { return runTests('./testcafe-fixtures/selector-error-test.js', 'Snapshot "count" property - selector error', { shouldFail: true, - only: 'chrome' + only: 'chrome', }) .catch(function (errs) { expect(errs[0]).contains( @@ -233,7 +278,7 @@ describe('[API] Selector', function () { it('Should raise error if error occurs in selector during "exists" property evaluation', function () { return runTests('./testcafe-fixtures/selector-error-test.js', 'Snapshot "exists" property - selector error', { shouldFail: true, - only: 'chrome' + only: 'chrome', }) .catch(function (errs) { expect(errs[0]).contains( @@ -243,35 +288,42 @@ describe('[API] Selector', function () { }); }); - it('Should raise error if custom DOM property throws an error', - function () { - return runTests('./testcafe-fixtures/selector-error-test.js', 'Add custom DOM properties method - property throws an error', { - shouldFail: true, - only: 'chrome' - }) - .catch(function (errs) { - expect(errs[0]).contains( - 'An error occurred when trying to calculate a custom Selector property "prop": Error: test' - ); - expect(errs[0]).contains('> 53 | await el();'); - }); - } - ); - - it('Should raise error if custom method throws an error', - function () { - return runTests('./testcafe-fixtures/selector-error-test.js', 'Add custom method - method throws an error', { - shouldFail: true, - only: 'chrome' - }) - .catch(function (errs) { - expect(errs[0]).contains( - 'An error occurred in customMethod code: Error: test' - ); - expect(errs[0]).contains('> 63 | await el.customMethod();'); - }); - } - ); + it('Should raise error if custom DOM property throws an error', function () { + return runTests('./testcafe-fixtures/selector-error-test.js', 'Add custom DOM properties method - property throws an error', { + shouldFail: true, + only: 'chrome', + }) + .catch(function (errs) { + expect(errs[0]).contains( + 'An error occurred when trying to calculate a custom Selector property "prop": Error: test' + ); + expect(errs[0]).contains('> 53 | await el();'); + }); + }); + + it('Should raise error if custom method throws an error', function () { + return runTests('./testcafe-fixtures/selector-error-test.js', 'Add custom method - method throws an error', { + shouldFail: true, + only: 'chrome', + }) + .catch(function (errs) { + expect(errs[0]).contains('An error occurred in customMethod code:'); + expect(errs[0]).contains('Error: test'); + expect(errs[0]).contains('> 63 | await el.customMethod();'); + }); + }); + + it('Should raise error if custom method throws an error - Selector mode', function () { + return runTests('./testcafe-fixtures/selector-error-test.js', 'Add custom method - method throws an error - Selector mode', { + shouldFail: true, + only: 'chrome', + }) + .catch(function (errs) { + expect(errs[0]).contains('An error occurred in Selector code:'); + expect(errs[0]).contains('Error: test'); + expect(errs[0]).contains('> 73 | await el.customMethod()();'); + }); + }); }); describe('Regression', function () { @@ -282,7 +334,7 @@ describe('[API] Selector', function () { it('Should select
- \ No newline at end of file + diff --git a/test/functional/fixtures/regression/gh-423/test.js b/test/functional/fixtures/regression/gh-423/test.js index 09ad949b2d7..f37a5c8632b 100644 --- a/test/functional/fixtures/regression/gh-423/test.js +++ b/test/functional/fixtures/regression/gh-423/test.js @@ -1,3 +1,5 @@ +const { skipInNativeAutomation } = require('../../../utils/skip-in'); + describe('[Regression](GH-423)', function () { it('Should raise click event except in Firefox if target element appends child after mousedown', function () { return runTests('testcafe-fixtures/index.test.js', 'Raise click if target appends child', { skip: ['firefox', 'firefox-osx'] }); @@ -19,7 +21,7 @@ describe('[Regression](GH-423)', function () { return runTests('testcafe-fixtures/index.test.js', "Don't raise click if target parent changed", { skip: ['firefox', 'firefox-osx'] }); }); - it("Shouldn't raise click if target appends editable form element", function () { + skipInNativeAutomation("Shouldn't raise click if target appends editable form element", function () { return runTests('testcafe-fixtures/index.test.js', "Don't raise click event if target appends input element"); }); }); diff --git a/test/functional/fixtures/regression/gh-4232/pages/cross-domain-page.html b/test/functional/fixtures/regression/gh-4232/pages/cross-domain-page.html new file mode 100644 index 00000000000..4f4701778a7 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4232/pages/cross-domain-page.html @@ -0,0 +1,9 @@ + + + + GH-4232 + + +

Cross-domain page

+ + diff --git a/test/functional/fixtures/regression/gh-4232/pages/iframe.html b/test/functional/fixtures/regression/gh-4232/pages/iframe.html new file mode 100644 index 00000000000..485d4b3c306 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4232/pages/iframe.html @@ -0,0 +1,16 @@ + + + + GH-4232 + + + + + + diff --git a/test/functional/fixtures/regression/gh-4232/pages/index.html b/test/functional/fixtures/regression/gh-4232/pages/index.html new file mode 100644 index 00000000000..eeb13085285 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4232/pages/index.html @@ -0,0 +1,9 @@ + + + + GH-4232 + + + + + diff --git a/test/functional/fixtures/regression/gh-4232/test.js b/test/functional/fixtures/regression/gh-4232/test.js new file mode 100644 index 00000000000..9c07a9c2205 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4232/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-4232)', function () { + it('Should not hang after submitting button in an iframe which redirects to a page on a different domain', function () { + return runTests('testcafe-fixtures/index-test.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4232/testcafe-fixtures/index-test.js b/test/functional/fixtures/regression/gh-4232/testcafe-fixtures/index-test.js new file mode 100644 index 00000000000..cec0d2b2154 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4232/testcafe-fixtures/index-test.js @@ -0,0 +1,13 @@ +import { Selector } from 'testcafe'; + +fixture `gh-4232` + .page `http://localhost:3000/fixtures/regression/gh-4232/pages/index.html`; + +test('Click on submit button in an iframe when the click redirecting to a page on a different domain', async t => { + const iframe = Selector('#same-domain-iframe', { timeout: 10000 }); + + await t + .switchToIframe(iframe) + .click('input') + .expect(Selector('h1').innerText).eql('Cross-domain page'); +}); diff --git a/test/functional/fixtures/regression/gh-4360/pages/frame.html b/test/functional/fixtures/regression/gh-4360/pages/frame.html new file mode 100644 index 00000000000..2e4d72e7b3c --- /dev/null +++ b/test/functional/fixtures/regression/gh-4360/pages/frame.html @@ -0,0 +1,23 @@ + + + + + gh-4360 + + +
+
+ + + + diff --git a/test/functional/fixtures/regression/gh-4360/pages/index.html b/test/functional/fixtures/regression/gh-4360/pages/index.html new file mode 100644 index 00000000000..1976feb3215 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4360/pages/index.html @@ -0,0 +1,10 @@ + + + + + gh-4360 + + + + + diff --git a/test/functional/fixtures/regression/gh-4360/test.js b/test/functional/fixtures/regression/gh-4360/test.js new file mode 100644 index 00000000000..06e7ff0b6ca --- /dev/null +++ b/test/functional/fixtures/regression/gh-4360/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-4360) - Should not throw \'contextStorage is undefined\' error', function () { + it('Submit form in iframe immediately after load', function () { + return runTests('testcafe-fixtures/index.js', null, { skip: ['chrome-osx', 'firefox-osx', 'ipad', 'iphone'] }); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-4360/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4360/testcafe-fixtures/index.js new file mode 100644 index 00000000000..8fa690974a5 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4360/testcafe-fixtures/index.js @@ -0,0 +1,11 @@ +import { Selector } from 'testcafe'; + +fixture `GH-4360 - Should not throw 'contextStorage is undefined' error` + .page `http://localhost:3000/fixtures/regression/gh-4360/pages/index.html`; + +test(`Submit form in iframe immediately after load`, async t => { + await t + .wait(500) + .switchToIframe('iframe'); + await t.expect(Selector('#target').innerText).eql('OK'); +}); diff --git a/test/functional/fixtures/regression/gh-4472/pages/index.html b/test/functional/fixtures/regression/gh-4472/pages/index.html new file mode 100644 index 00000000000..cbe4c2ee3db --- /dev/null +++ b/test/functional/fixtures/regression/gh-4472/pages/index.html @@ -0,0 +1,20 @@ + + + + + gh-4472 + + +
+ +
+ + + diff --git a/test/functional/fixtures/regression/gh-4472/test.js b/test/functional/fixtures/regression/gh-4472/test.js new file mode 100644 index 00000000000..3468e64b5f1 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4472/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-4472)', function () { + it('Input should not lose focus after the focus method was called on a not focusable element', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4472/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4472/testcafe-fixtures/index.js new file mode 100644 index 00000000000..459e4ec6b7e --- /dev/null +++ b/test/functional/fixtures/regression/gh-4472/testcafe-fixtures/index.js @@ -0,0 +1,11 @@ +import { Selector } from 'testcafe'; + +fixture `GH-4472` + .page `http://localhost:3000/fixtures/regression/gh-4472/pages/index.html`; + +test(`Should not lose the focus on input`, async t => { + const input = Selector('#targetInput'); + + await t.typeText(input, 'text'); + await t.expect(input.value).eql('text'); +}); diff --git a/test/functional/fixtures/regression/gh-4516/pages/index.html b/test/functional/fixtures/regression/gh-4516/pages/index.html new file mode 100644 index 00000000000..063ef451b74 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4516/pages/index.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4516/test.js b/test/functional/fixtures/regression/gh-4516/test.js new file mode 100644 index 00000000000..3261b30da34 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4516/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-4516) - Should call the onResponse event for AJAX requests', function () { + it('Should call the onResponse event for AJAX requests', function () { + return runTests('testcafe-fixtures/index.js', null, { only: 'chrome' }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4516/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4516/testcafe-fixtures/index.js new file mode 100644 index 00000000000..1e78a48d2dc --- /dev/null +++ b/test/functional/fixtures/regression/gh-4516/testcafe-fixtures/index.js @@ -0,0 +1,59 @@ +import { RequestHook, Selector } from 'testcafe'; +import ReExecutablePromise from '../../../../../../lib/utils/re-executable-promise.js'; + +export default class CustomHook extends RequestHook { + constructor (config) { + super(null, config); + + this.pendingAjaxRequestIds = new Set(); + this._hasAjaxRequests = false; + } + + onRequest (event) { + if (event.isAjax) { + this.pendingAjaxRequestIds.add(event._requestInfo.requestId); + + this._hasAjaxRequests = true; + } + } + + onResponse (event) { + this.pendingAjaxRequestIds.delete(event.requestId); + } + + get hasAjaxRequests () { + return ReExecutablePromise.fromFn(async () => this._hasAjaxRequests); + } + + get allAjaxRequestsCompleted () { + return ReExecutablePromise.fromFn(async () => this.pendingAjaxRequestIds.size === 0); + } +} + +const hook1 = new CustomHook(); +const hook2 = new CustomHook({}); +const hook3 = new CustomHook({ includeHeaders: true }); + +fixture `GH-4516` + .page `http://localhost:3000/fixtures/regression/gh-4516/pages/index.html`; + +test.requestHooks(hook1)('Without config', async t => { + await t + .expect(Selector('#result').visible).ok() + .expect(hook1.hasAjaxRequests).ok() + .expect(hook1.allAjaxRequestsCompleted).ok(); +}); + +test.requestHooks(hook2)('With empty config', async t => { + await t + .expect(Selector('#result').visible).ok() + .expect(hook2.hasAjaxRequests).ok() + .expect(hook1.allAjaxRequestsCompleted).ok(); +}); + +test.requestHooks(hook3)('With includeHeaders', async t => { + await t + .expect(Selector('#result').visible).ok() + .expect(hook3.hasAjaxRequests).ok() + .expect(hook1.allAjaxRequestsCompleted).ok(); +}); diff --git a/test/functional/fixtures/regression/gh-4558/pages/iframePage.html b/test/functional/fixtures/regression/gh-4558/pages/iframePage.html new file mode 100644 index 00000000000..8ab5f22c3c2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/pages/iframePage.html @@ -0,0 +1,49 @@ + + + + + Title + + + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4558/pages/index.html b/test/functional/fixtures/regression/gh-4558/pages/index.html new file mode 100644 index 00000000000..366ed2c1dca --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/pages/index.html @@ -0,0 +1,21 @@ + + + + + Title + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4558/pages/innerIframePage.html b/test/functional/fixtures/regression/gh-4558/pages/innerIframePage.html new file mode 100644 index 00000000000..4bcc0fd225c --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/pages/innerIframePage.html @@ -0,0 +1,10 @@ + + + + + Title + + +OK + + diff --git a/test/functional/fixtures/regression/gh-4558/test-data/data.js b/test/functional/fixtures/regression/gh-4558/test-data/data.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/functional/fixtures/regression/gh-4558/test.js b/test/functional/fixtures/regression/gh-4558/test.js new file mode 100644 index 00000000000..fc1fb3ad8ba --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/test.js @@ -0,0 +1,46 @@ +const expect = require('chai').expect; + +describe('[Regression](GH-4558)', () => { + it('Should fail on click an element in invisible iframe', () => { + return runTests('./testcafe-fixtures/index.js', 'Button click', { shouldFail: true }) + .catch(err => { + expect(err[0]).contains('The action target () is located outside the the layout viewport.'); + }); + }); + + it('Should press key in iframe document', () => { + return runTests('./testcafe-fixtures/index.js', 'Press key'); + }); + + it('Set files to upload and clear upload', () => { + return runTests('./testcafe-fixtures/index.js', 'Set files to upload and clear upload'); + }); + + it('Dispatch a Click event', () => { + return runTests('./testcafe-fixtures/index.js', 'Dispatch a Click event'); + }); + + it('Eval', () => { + return runTests('./testcafe-fixtures/index.js', 'Eval'); + }); + + it('Set native dialog handler and get common dialog history', () => { + return runTests('./testcafe-fixtures/index.js', 'Set native dialog handler and get common dialog history'); + }); + + it('Get browser console messages', () => { + return runTests('./testcafe-fixtures/index.js', 'Get browser console messages'); + }); + + it('Switch to inner iframe', () => { + return runTests('./testcafe-fixtures/index.js', 'Switch to inner iframe'); + }); + + it('Hidden by visibility style', () => { + return runTests('./testcafe-fixtures/index.js', 'Hidden by visibility style', { shouldFail: true }) + .catch(err => { + expect(err[0]).contains('The element that matches the specified selector is not visible.'); + }); + }); +}); + diff --git a/test/functional/fixtures/regression/gh-4558/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4558/testcafe-fixtures/index.js new file mode 100644 index 00000000000..96fcadd79f1 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4558/testcafe-fixtures/index.js @@ -0,0 +1,115 @@ +import { Selector, ClientFunction } from 'testcafe'; + +fixture(`RG-4558 - Invisible iframe`) + .page(`http://localhost:3000/fixtures/regression/gh-4558/pages/index.html`); + +async function expectResultText (t, text = 'OK') { + await t.expect(Selector('#result').innerText).eql(text); +} + +async function setNativeDialogHandler (t) { + await t + .setNativeDialogHandler((type, text) => { + switch (type) { + case 'confirm': + return text === 'confirm'; + case 'prompt': + return 'PROMPT'; + default: + return true; + } + }); +} + +const iframeSelector = Selector('#invisibleIframe'); + +const focusDocument = ClientFunction(() => { + document.getElementById('focusInput').focus(); +}); + +test('Button click', async t => { + await t.switchToIframe(iframeSelector); + await t.click(Selector('#button', { timeout: 200 })); + + throw new Error('Test rejection expected'); +}); + +test('Press key', async t => { + await t.switchToIframe(iframeSelector); + await focusDocument(); + await t.pressKey('a'); + await expectResultText(t); +}); + +test('Set files to upload and clear upload', async t => { + await t.switchToIframe(iframeSelector); + await t.setFilesToUpload('#upload', '../test-data/data.js'); + await expectResultText(t, 'ADD'); + await t.clearUpload('#upload'); + await expectResultText(t, 'CLEAR'); +}); + + +test('Dispatch a Click event', async t => { + await t.switchToIframe(iframeSelector); + + const eventArgs = { + cancelable: false, + bubbles: false, + }; + + const options = Object.assign( + { eventConstructor: 'MouseEvent' }, + eventArgs, + ); + + await t + .dispatchEvent('#button', 'click', options); + + await expectResultText(t); +}); + +test('Eval', async t => { + await t.switchToIframe(iframeSelector); + await t.eval(() => window.setSpanText()); + await expectResultText(t); +}); + +test('Set native dialog handler and get common dialog history', async t => { + await t.switchToIframe(iframeSelector); + await setNativeDialogHandler(t); + await t.eval(() => window.showDialog('confirm')); + await expectResultText(t, 'CONFIRM'); + await t.eval(() => window.showDialog('prompt')); + await expectResultText(t, 'PROMPT'); + await t.switchToMainWindow(); + await t.eval(() => window.showNativeDialog('alert')); + + const history = await t.getNativeDialogHistory(); + + await t.expect(history[0].url).contains('index.html'); + await t.expect(history[1].url).contains('iframePage.html'); + await t.expect(history[2].url).contains('iframePage.html'); +}); + +test('Get browser console messages', async t => { + await t.switchToIframe(iframeSelector); + await t.eval(() => window.logToConsole('console-test')); + const browserConsoleMessages = await t.getBrowserConsoleMessages(); + + await t.expect(browserConsoleMessages.log).contains('console-test'); +}); + +test('Switch to inner iframe', async t => { + await t.switchToIframe(iframeSelector); + await t.switchToIframe(Selector('#iframe2')); + await expectResultText(t); +}); + + +test('Hidden by visibility style', async t => { + await t.switchToIframe('#hiddenIframe'); + + throw new Error('Test rejection expected'); +}); + diff --git a/test/functional/fixtures/regression/gh-4613/common/method-with-failed-assertion.js b/test/functional/fixtures/regression/gh-4613/common/method-with-failed-assertion.js new file mode 100644 index 00000000000..5856fd18005 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4613/common/method-with-failed-assertion.js @@ -0,0 +1,5 @@ +import { t } from 'testcafe'; + +export default async function methodWithFailedAssertion () { + await t.expect(1).eql(2); +} diff --git a/test/functional/fixtures/regression/gh-4613/executed-test-info.js b/test/functional/fixtures/regression/gh-4613/executed-test-info.js new file mode 100644 index 00000000000..3823cc09ae9 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4613/executed-test-info.js @@ -0,0 +1,37 @@ +const { expect } = require('chai'); + +const EXPECTED_EXECUTED_TEST_NAMES = ['1', '2', '3']; + +module.exports = class ExecutedTestInfo { + constructor () { + this.clear(); + } + + clear () { + this.testNames = []; + this.errs = []; + this.warnings = []; + } + + onTestDone (name, testRunInfo) { + this.testNames.push(name); + this.errs.push(...testRunInfo.errs); + } + + onTaskDone (warnings) { + this.warnings.push(...warnings); + } + + check () { + expect(this.testNames).eql(EXPECTED_EXECUTED_TEST_NAMES); + expect(this.errs.length).eql(0); + expect(this.warnings[0]).contain( + "An asynchronous method that you do not await includes an assertion. Inspect that method's execution chain and add the 'await' keyword where necessary." + '\n\n' + + ' 1 |import { t } from \'testcafe\';' + '\n' + + ' 2 |' + '\n' + + ' 3 |export default async function methodWithFailedAssertion () {' + '\n' + + ' > 4 | await t.expect(1).eql(2);' + '\n' + + ' 5 |}' + '\n' + + ' 6 |'); + } +}; diff --git a/test/functional/fixtures/regression/gh-4613/test.js b/test/functional/fixtures/regression/gh-4613/test.js new file mode 100644 index 00000000000..ffdd506c757 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4613/test.js @@ -0,0 +1,37 @@ +const { createReporter } = require('../../../utils/reporter'); +const ExecutedTestInfo = require('./executed-test-info'); + + +const executedTestInfo = new ExecutedTestInfo(); + +const reporter = createReporter({ + reportTestDone (name, testRunInfo) { + executedTestInfo.onTestDone(name, testRunInfo); + }, + + reportTaskDone (endTime, passed, warnings) { + executedTestInfo.onTaskDone(warnings); + }, +}); + +describe('Should not interrupt test execution after unawaited method with assertion (GH-4613)', () => { + beforeEach(() => { + executedTestInfo.clear(); + }); + + afterEach(() => { + executedTestInfo.check(); + }); + + it('the test with the unawaited method is first', () => { + return runTests('./testcafe-fixtures/first.js', null, { reporter }); + }); + + it('the test with the unawaited method is middle', () => { + return runTests('./testcafe-fixtures/middle.js', null, { reporter }); + }); + + it('the test with the unawaited method is last', () => { + return runTests('./testcafe-fixtures/last.js', null, { reporter }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/first.js b/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/first.js new file mode 100644 index 00000000000..f0ee29a998a --- /dev/null +++ b/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/first.js @@ -0,0 +1,15 @@ +import methodWithFailedAssertion from '../common/method-with-failed-assertion.js'; + +fixture `First`; + +test('1', async () => { + methodWithFailedAssertion(); +}); + +test('2', async t => { + await t.wait(2000); +}); + +test('3', async t => { + await t.wait(2000); +}); diff --git a/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/last.js b/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/last.js new file mode 100644 index 00000000000..cad1d6c2004 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/last.js @@ -0,0 +1,15 @@ +import methodWithFailedAssertion from '../common/method-with-failed-assertion.js'; + +fixture `Last`; + +test('1', async t => { + await t.wait(2000); +}); + +test('2', async t => { + await t.wait(2000); +}); + +test('3', async () => { + methodWithFailedAssertion(); +}); diff --git a/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/middle.js b/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/middle.js new file mode 100644 index 00000000000..d1fdaab7594 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4613/testcafe-fixtures/middle.js @@ -0,0 +1,15 @@ +import methodWithFailedAssertion from '../common/method-with-failed-assertion.js'; + +fixture `Middle`; + +test('1', async t => { + await t.wait(2000); +}); + +test('2', async () => { + methodWithFailedAssertion(); +}); + +test('3', async t => { + await t.wait(2000); +}); diff --git a/test/functional/fixtures/regression/gh-4675/pages/index.html b/test/functional/fixtures/regression/gh-4675/pages/index.html new file mode 100644 index 00000000000..8a033ec932b --- /dev/null +++ b/test/functional/fixtures/regression/gh-4675/pages/index.html @@ -0,0 +1,9 @@ + + + + + gh-4675 + + + + diff --git a/test/functional/fixtures/regression/gh-4675/test.js b/test/functional/fixtures/regression/gh-4675/test.js new file mode 100644 index 00000000000..e15643ecadd --- /dev/null +++ b/test/functional/fixtures/regression/gh-4675/test.js @@ -0,0 +1,45 @@ +const path = require('path'); +const { expect } = require('chai'); +const createTestCafe = require('../../../../../lib'); +const config = require('../../../config.js'); +const { createReporter } = require('../../../utils/reporter'); +const osFamily = require('os-family'); + +function customReporter (name) { + return createReporter({ + name, + reportTaskStart () { + this.write(''); + }, + }); +} + +let testCafe = null; + +if (config.useLocalBrowsers && !config.useHeadlessBrowsers && !osFamily.mac) { + describe('[Regression](GH-4675) - Should raise an error if several reporters are going to write to the stdout', function () { + it('Should raise an error if several reporters are going to write to the stdout', function () { + let error = null; + + return createTestCafe('127.0.0.1', 1335, 1336) + .then(tc => { + testCafe = tc; + }) + .then(() => { + return testCafe.createRunner() + .browsers(`chrome`) + .src(path.join(__dirname, './testcafe-fixtures/index.js')) + .reporter([customReporter('custom1'), customReporter('custom2')]) + .run({ disableNativeAutomation: !config.nativeAutomation }); + }) + .catch(err => { + error = err; + + return testCafe.close(); + }) + .finally(() => { + expect(error.message).eql('Reporters cannot share output streams. The following reporters interfere with one another: "custom1, custom2".'); + }); + }); + }); +} diff --git a/test/functional/fixtures/regression/gh-4675/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4675/testcafe-fixtures/index.js new file mode 100644 index 00000000000..c507fa0d252 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4675/testcafe-fixtures/index.js @@ -0,0 +1,5 @@ +fixture `GH-4675 - Should raise an error if several reporters are going to write to the stdout` + .page `http://localhost:3000/fixtures/regression/gh-4675/pages/index.html`; + +test(`Dummy`, async () => { +}); diff --git a/test/functional/fixtures/regression/gh-4725/pages/index.html b/test/functional/fixtures/regression/gh-4725/pages/index.html new file mode 100644 index 00000000000..9faf4129ddb --- /dev/null +++ b/test/functional/fixtures/regression/gh-4725/pages/index.html @@ -0,0 +1,9 @@ + + + + + gh-4725 + + + + diff --git a/test/functional/fixtures/regression/gh-4725/test.js b/test/functional/fixtures/regression/gh-4725/test.js new file mode 100644 index 00000000000..23e4df0fad5 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4725/test.js @@ -0,0 +1,29 @@ +const { expect } = require('chai'); +const { createReporter } = require('../../../utils/reporter'); + +const log = []; + +const reporter = createReporter({ + reportTestStart (name) { + log.push(`start: ${name}`); + }, + reportTestDone (name) { + log.push(`done: ${name}`); + }, +}); + +const expectedLog = [ + `start: test 0`, + `done: test 0`, + `start: test 1`, + `done: test 1`, +]; + +describe('[Regression](GH-4725)', function () { + it('Should respect test start/done event order', function () { + return runTests('testcafe-fixtures/index.js', null, { reporter }) + .then(() => { + expect(log).eql(expectedLog); + }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4725/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4725/testcafe-fixtures/index.js new file mode 100644 index 00000000000..fd7c0b63d01 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4725/testcafe-fixtures/index.js @@ -0,0 +1,9 @@ +fixture `GH-4725 - Should ensure AsyncEventEmitter from additional event subscriptions` + .page `http://localhost:3000/fixtures/regression/gh-4725/pages/index.html`; + +for (let i = 0; i < 2; i++) { + test(`test ${i}`, async t => { + if (t.browser.alias === 'Firefox') + await t.wait(5000); + }); +} diff --git a/test/functional/fixtures/regression/gh-4787/pages/index.html b/test/functional/fixtures/regression/gh-4787/pages/index.html new file mode 100644 index 00000000000..cc27ee08dbf --- /dev/null +++ b/test/functional/fixtures/regression/gh-4787/pages/index.html @@ -0,0 +1,9 @@ + + + + + gh-4787 + + + + diff --git a/test/functional/fixtures/regression/gh-4787/test.js b/test/functional/fixtures/regression/gh-4787/test.js new file mode 100644 index 00000000000..ba33728ebe2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4787/test.js @@ -0,0 +1,67 @@ +const { expect } = require('chai'); +const path = require('path'); +const createTestCafe = require('../../../../../lib'); +const config = require('../../../config.js'); +const delay = require('../../../../../lib/utils/delay'); +const { createReporter } = require('../../../utils/reporter'); +const osFamily = require('os-family'); + +let cafe = null; +let runner = null; +const log = []; + +const expectedLog = [ + 'Fixture 1', + ...new Array(10).fill('Test 1'), + 'Fixture 2', + ...new Array(10).fill('Test 2'), + 'Fixture 3', + ...new Array(10).fill('Test 3'), +]; + +async function sleep () { + return delay(100); +} + +const reporter = createReporter({ + async reportFixtureStart (name) { + log.push(name); + + await sleep(); + }, + async reportTestStart (name) { + log.push(name); + + await sleep(); + }, + async reportTestDone (name) { + log.push(name); + + await sleep(); + }, +}); + +if (config.useLocalBrowsers && !config.useHeadlessBrowsers && !osFamily.mac) { + describe('[Regression](GH-4787) - Should wait for last report before new fixture starts', function () { + it('Should wait for last report before new fixture starts', function () { + return createTestCafe('127.0.0.1', 1335, 1336) + .then(testcafe => { + runner = testcafe.createRunner(); + cafe = testcafe; + }) + .then(() => { + return runner + .src(path.join(__dirname, './testcafe-fixtures/index.js')) + .browsers(['chrome', 'firefox']) + .reporter(reporter) + .run({ disableNativeAutomation: !config.nativeAutomation }); + }) + .then(() => { + return cafe.close(); + }) + .then(() => { + expect(log).eql(expectedLog); + }); + }); + }); +} diff --git a/test/functional/fixtures/regression/gh-4787/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4787/testcafe-fixtures/index.js new file mode 100644 index 00000000000..2c230177747 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4787/testcafe-fixtures/index.js @@ -0,0 +1,26 @@ +fixture `Fixture 1` + .page `http://localhost:3000/fixtures/regression/gh-4787/pages/index.html`; + +for (let i = 0; i < 5; i++) { + test('Test 1', async t => { + await t.wait(1000); + }); +} + +fixture `Fixture 2` + .page `http://localhost:3000/fixtures/regression/gh-4787/pages/index.html`; + +for (let i = 0; i < 5; i++) { + test('Test 2', async t => { + await t.wait(1000); + }); +} + +fixture `Fixture 3` + .page `http://localhost:3000/fixtures/regression/gh-4787/pages/index.html`; + +for (let i = 0; i < 5; i++) { + test('Test 3', async t => { + await t.wait(1000); + }); +} diff --git a/test/functional/fixtures/regression/gh-4793/pages/iframe.html b/test/functional/fixtures/regression/gh-4793/pages/iframe.html new file mode 100644 index 00000000000..5911612e263 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4793/pages/iframe.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4793/pages/index.html b/test/functional/fixtures/regression/gh-4793/pages/index.html new file mode 100644 index 00000000000..3892aa7cb3a --- /dev/null +++ b/test/functional/fixtures/regression/gh-4793/pages/index.html @@ -0,0 +1,9 @@ + + + + GH-4793 + + + + + diff --git a/test/functional/fixtures/regression/gh-4793/test.js b/test/functional/fixtures/regression/gh-4793/test.js new file mode 100644 index 00000000000..f93f1ce061c --- /dev/null +++ b/test/functional/fixtures/regression/gh-4793/test.js @@ -0,0 +1,6 @@ +describe('[Regression](GH-4793)', function () { + it('Elements inside cross-domain iframes should be focusable', function () { + // TODO: skipped in Safari due to https://github.com/DevExpress/testcafe-private/issues/556 + return runTests('testcafe-fixtures/index-test.js', 'Type text into an input inside a cross-domain iframe', { skip: ['safari'] }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4793/testcafe-fixtures/index-test.js b/test/functional/fixtures/regression/gh-4793/testcafe-fixtures/index-test.js new file mode 100644 index 00000000000..0f3c8db1ccd --- /dev/null +++ b/test/functional/fixtures/regression/gh-4793/testcafe-fixtures/index-test.js @@ -0,0 +1,14 @@ +import { Selector } from 'testcafe'; + +fixture `gh-4793` + .page `http://localhost:3000/fixtures/regression/gh-4793/pages/index.html`; + +test('Type text into an input inside a cross-domain iframe', async t => { + const iframe = Selector('#cross-domain-iframe', { timeout: 10000 }); + const input = Selector('#input'); + + await t + .switchToIframe(iframe) + .typeText(input, '1234') + .expect(input.value).eql('1234'); +}); diff --git a/test/functional/fixtures/regression/gh-4848/pages/index.html b/test/functional/fixtures/regression/gh-4848/pages/index.html new file mode 100644 index 00000000000..00a99022b5c --- /dev/null +++ b/test/functional/fixtures/regression/gh-4848/pages/index.html @@ -0,0 +1,32 @@ + + + + + gh-4848 + + + + +
target 1
+
target 2
+
target 3
+ test +
target 4
+
target 5
+ + + + diff --git a/test/functional/fixtures/regression/gh-4848/test.js b/test/functional/fixtures/regression/gh-4848/test.js new file mode 100644 index 00000000000..d95b011b4d0 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4848/test.js @@ -0,0 +1,25 @@ +describe('[Regression](GH-4848) - Should focus next element if current element has negative tabIndex', function () { + it('Straight order. Middle', function () { + return runTests('testcafe-fixtures/index.js', 'Straight order. Middle'); + }); + + it('Reversed order. Middle', function () { + return runTests('testcafe-fixtures/index.js', 'Reversed order. Middle'); + }); + + it('Reversed order. First', function () { + return runTests('testcafe-fixtures/index.js', 'Reversed order. First'); + }); + + it('Reversed order. Last', function () { + return runTests('testcafe-fixtures/index.js', 'Reversed order. Last'); + }); + + it('Straight order. First', function () { + return runTests('testcafe-fixtures/index.js', 'Straight order. First'); + }); + + it('Straight order. Last', function () { + return runTests('testcafe-fixtures/index.js', 'Straight order. Last'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4848/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4848/testcafe-fixtures/index.js new file mode 100644 index 00000000000..35e646ec94d --- /dev/null +++ b/test/functional/fixtures/regression/gh-4848/testcafe-fixtures/index.js @@ -0,0 +1,48 @@ +import { Selector } from 'testcafe'; + +fixture `[Regression](GH-4848) - Should focus next element if current element has negative tabIndex` + .page `http://localhost:3000/fixtures/regression/gh-4848/pages/index.html`; + +const body = Selector('body'); +const ft1 = Selector('#ft1'); +const ft5 = Selector('#ft5'); +const a = Selector('a'); + +test(`Straight order. Middle`, async t => { + await t.pressKey('tab'); + await t.expect(Selector(a).focused).eql(true); +}); + +test(`Reversed order. Middle`, async t => { + await t.pressKey('shift+tab'); + await t.expect(Selector('#target1').focused).eql(true); +}); + +test(`Reversed order. Last`, async t => { + await t.click(ft5); + await t.pressKey('shift+tab'); + await t.expect(Selector('#target4').focused).eql(true); +}); + +test(`Straight order. First`, async t => { + await t.click(ft1); + await t.pressKey('tab'); + await t.expect(a.focused).eql(true); +}); + +test(`Reversed order. First`, async t => { + await t.click(ft1); + await t.pressKey('shift+tab'); + await t.expect(body.focused).eql(true); +}); + +test(`Straight order. Last`, async t => { + await t.click(ft5); + await t.pressKey('tab'); + await t.expect(Selector('#target1').focused).eql(true); + + await t.click(ft1); // NOTE: make tabIndex of `target1` to -1 + await t.click(ft5); + await t.pressKey('tab'); + await t.expect(Selector(a).focused).eql(true); +}); diff --git a/test/functional/fixtures/regression/gh-4988/pages/frame1.html b/test/functional/fixtures/regression/gh-4988/pages/frame1.html new file mode 100644 index 00000000000..379328fa3b2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4988/pages/frame1.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4988/pages/frame2.html b/test/functional/fixtures/regression/gh-4988/pages/frame2.html new file mode 100644 index 00000000000..b1bf241d1f4 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4988/pages/frame2.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4988/pages/index.html b/test/functional/fixtures/regression/gh-4988/pages/index.html new file mode 100644 index 00000000000..dad339353d7 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4988/pages/index.html @@ -0,0 +1,15 @@ + + + + + gh-4988 (iframe part) + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-4988/test.js b/test/functional/fixtures/regression/gh-4988/test.js new file mode 100644 index 00000000000..2196713b456 --- /dev/null +++ b/test/functional/fixtures/regression/gh-4988/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-4988) - improved algo for shadow/iframe elements', function () { + it('improved algo for shadow/iframe elements', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-4988/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-4988/testcafe-fixtures/index.js new file mode 100644 index 00000000000..507f431b59f --- /dev/null +++ b/test/functional/fixtures/regression/gh-4988/testcafe-fixtures/index.js @@ -0,0 +1,42 @@ +import { Selector } from 'testcafe'; + +const btn1 = Selector('#btn1'); +const btn2 = Selector('#btn2'); +const btn3 = Selector('#btn3'); +const btn4 = Selector('#btn4'); +const btn5 = Selector('#btn5'); +const btn6 = Selector('#btn6'); +const btn7 = Selector('#btn7'); +const btn8 = Selector('#btn8'); +const btn9 = Selector('#btn9'); +const btn10 = Selector('#btn10'); +const frm1 = Selector('#iframe1'); +const frm2 = Selector('#iframe2'); + +async function assert (t, expectedElement, iframeElement) { + await t.pressKey('tab'); + + if (iframeElement) + await t.switchToIframe(iframeElement); + + await t.expect(expectedElement.focused).eql(true); + + if (iframeElement) + await t.switchToMainWindow(); +} + +fixture `GH-4988 - improved algo for shadow/iframe elements` + .page `http://localhost:3000/fixtures/regression/gh-4988/pages/index.html`; + +test('iframe', async t => { + await assert(t, btn6); + await assert(t, btn2); + await assert(t, btn9, frm2); + await assert(t, btn8, frm2); + await assert(t, btn7, frm2); + await assert(t, btn1); + await assert(t, btn5, frm1); + await assert(t, btn4, frm1); + await assert(t, btn3, frm1); + await assert(t, btn10); +}); diff --git a/test/functional/fixtures/regression/gh-5207/pages/index.html b/test/functional/fixtures/regression/gh-5207/pages/index.html new file mode 100644 index 00000000000..65e68fb6685 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5207/pages/index.html @@ -0,0 +1,9 @@ + + + + + gh-5207 + + + + diff --git a/test/functional/fixtures/regression/gh-5207/test.js b/test/functional/fixtures/regression/gh-5207/test.js new file mode 100644 index 00000000000..f0c321ac281 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5207/test.js @@ -0,0 +1,15 @@ +const { createReporter } = require('../../../utils/reporter'); + +describe('[Regression](GH-5207)', function () { + it('Should not hand with `disablePageReloads` and async reporter', function () { + return runTests('testcafe-fixtures/index.js', null, { + reporter: createReporter({ + async reportTestDone () { + return new Promise(resolve => { + setTimeout(resolve, 5000); + }); + }, + }), + }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-5207/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5207/testcafe-fixtures/index.js new file mode 100644 index 00000000000..b04f660718b --- /dev/null +++ b/test/functional/fixtures/regression/gh-5207/testcafe-fixtures/index.js @@ -0,0 +1,16 @@ +fixture `fixture 1` + .page `http://localhost:3000/fixtures/regression/gh-5207/pages/index.html`; + +test(`test 1`, async t => { + await t.wait(2000); +}); + +fixture `fixture 2` + .disablePageReloads + .page `http://example.com`; + +test(`test 2`, async t => { + await t.wait(2000); + + await t.click('h1'); +}); diff --git a/test/functional/fixtures/regression/gh-5239/pages/index.html b/test/functional/fixtures/regression/gh-5239/pages/index.html new file mode 100644 index 00000000000..6da0e6c258e --- /dev/null +++ b/test/functional/fixtures/regression/gh-5239/pages/index.html @@ -0,0 +1,8 @@ + + + + GH-5239 + + + + diff --git a/test/functional/fixtures/regression/gh-5239/test.js b/test/functional/fixtures/regression/gh-5239/test.js new file mode 100644 index 00000000000..9fff27bde91 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5239/test.js @@ -0,0 +1,74 @@ +const http = require('http'); +const path = require('path'); +const config = require('../../../config'); +const createTestCafe = require('../../../../../lib'); +const { getFreePort } = require('../../../../../lib/utils/endpoint-utils'); +const { skipInNativeAutomation } = require('../../../utils/skip-in'); + +const ERROR_RESPONSE_COUNT = 8; +const SIGNIFICANT_REQUEST_TIMEOUT = 200; + +let previousRequestTime = null; + +async function createServer () { + let requestCounter = 0; + + const requestListener = function (req, res) { + const now = Date.now(); + + // NOTE: consider only those requests, that were sent with interval more than 100ms + if (previousRequestTime && now - previousRequestTime > SIGNIFICANT_REQUEST_TIMEOUT) + requestCounter++; + + previousRequestTime = now; + + if (requestCounter < ERROR_RESPONSE_COUNT) + req.destroy(); + else { + res.writeHead(200); + res.end('

example

'); + } + }; + + const server = http.createServer(requestListener); + const port = await getFreePort(); + + process.env.TEST_SERVER_PORT = port.toString(); + + server.listen(port); + + return server; +} + +async function run ({ src, browsers, retryTestPages, reporter }) { + const testcafe = await createTestCafe('localhost', 1335, 1336, void 0, true, retryTestPages); + const runner = testcafe.createRunner(); + + await runner + .src(path.join(__dirname, src)) + .browsers(browsers); + + if (reporter) + runner.reporter(reporter); + + await runner.run({ disableNativeAutomation: !config.nativeAutomation }); + + await testcafe.close(); +} + +const isLocalChrome = config.useLocalBrowsers && config.browsers.some(browser => browser.alias.indexOf('chrome') > -1); + +describe('[Regression](GH-5239)', function () { + if (isLocalChrome) { + skipInNativeAutomation('Should make multiple request for the page if the server does not respond', async function () { + this.timeout(30000); + + const server = await createServer(); + + return run({ retryTestPages: true, browsers: 'chrome --headless', src: './testcafe-fixtures/index.js' }) + .then(() => { + server.close(); + }); + }); + } +}); diff --git a/test/functional/fixtures/regression/gh-5239/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5239/testcafe-fixtures/index.js new file mode 100644 index 00000000000..d5593f88450 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5239/testcafe-fixtures/index.js @@ -0,0 +1,8 @@ +import { Selector } from 'testcafe'; + +fixture `GH-5239 - Should make multiple request for the page if the server does not respond` + .page `http://localhost:${process.env.TEST_SERVER_PORT}`; + +test(`Click on the element`, async t => { + await t.click(Selector('h1').withText('example')); +}); diff --git a/test/functional/fixtures/regression/gh-5239/testcafe-fixtures/warnings-test.js b/test/functional/fixtures/regression/gh-5239/testcafe-fixtures/warnings-test.js new file mode 100644 index 00000000000..801e39e8d9b --- /dev/null +++ b/test/functional/fixtures/regression/gh-5239/testcafe-fixtures/warnings-test.js @@ -0,0 +1,5 @@ +fixture `GH-5239 - Should show warning if the 'retryTestPages' option is not supported` + .page `http://localhost:3000/fixtures/regression/gh-5239/pages/index.html`; + +test(`Dummy`, async () => { +}); diff --git a/test/functional/fixtures/regression/gh-5447/test.js b/test/functional/fixtures/regression/gh-5447/test.js new file mode 100644 index 00000000000..f02ca6a9467 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5447/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-5447) Should use the native date methods in the client code', () => { + it('Should use the native date methods in the client code', () => { + return runTests('testcafe-fixtures/index.js', null, { selectorTimeout: 5000 }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-5447/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5447/testcafe-fixtures/index.js new file mode 100644 index 00000000000..bffa201185b --- /dev/null +++ b/test/functional/fixtures/regression/gh-5447/testcafe-fixtures/index.js @@ -0,0 +1,24 @@ +const createTestDiv = + `window.setTimeout(function () { + var div = document.createElement('div'); + + div.className = 'testDiv'; + div.setAttribute('style', 'background-color: red; width: 100px; height: 100px'); + document.body.appendChild(div); + }, 2000); + `; + +const mockDate = + ` window.Date = function () { throw new Error('Use a stored native method instead of the Date constructor.'); }; + window.Date.now = function () { throw new Error('Use a stored native method instead of Date.now() function.'); }; + `; + +fixture `Fixture` + .clientScripts([ + { content: createTestDiv }, + { content: mockDate }, + ]); + +test('test', async t => { + await t.click('.testDiv'); +}); diff --git a/test/functional/fixtures/regression/gh-5549/test.js b/test/functional/fixtures/regression/gh-5549/test.js new file mode 100644 index 00000000000..e102bb43014 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5549/test.js @@ -0,0 +1,34 @@ +const createTestCafe = require('../../../../../lib'); +const config = require('../../../config'); +const path = require('path'); + +let testCafe = null; + +if (config.useLocalBrowsers) { + describe(`[Regression](GH-5449) Should not crash if TestCafe is created via "createTestCafe('null')"`, () => { + it(`[Regression](GH-5449) Should not crash if TestCafe is created via "createTestCafe('null')"`, () => { + let failedCount = 0; + + return createTestCafe(null) + .then(tc => { + testCafe = tc; + }) + .then(() => { + return testCafe + .createRunner() + .browsers('chrome:headless') + .src(path.join(__dirname, './testcafe-fixtures/index.js')) + .run({ disableNativeAutomation: !config.nativeAutomation }); + }) + .then(failed => { + failedCount = failed; + + return testCafe.close(); + }) + .then(() => { + if (failedCount) + throw new Error('Error occurred'); + }); + }); + }); +} diff --git a/test/functional/fixtures/regression/gh-5549/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5549/testcafe-fixtures/index.js new file mode 100644 index 00000000000..75d764ae2e7 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5549/testcafe-fixtures/index.js @@ -0,0 +1,3 @@ +fixture `Fixture`; + +test('test', async () => {}); diff --git a/test/functional/fixtures/regression/gh-5616/pages/index.html b/test/functional/fixtures/regression/gh-5616/pages/index.html new file mode 100644 index 00000000000..8caeaf1f396 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5616/pages/index.html @@ -0,0 +1,14 @@ + + + + + gh-5616 + + + + + diff --git a/test/functional/fixtures/regression/gh-5616/test.js b/test/functional/fixtures/regression/gh-5616/test.js new file mode 100644 index 00000000000..41099dc8de4 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5616/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-5616)', function () { + it('Element "select" shouldn\'t be opened', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-5616/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5616/testcafe-fixtures/index.js new file mode 100644 index 00000000000..077a09576f4 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5616/testcafe-fixtures/index.js @@ -0,0 +1,9 @@ +import { Selector } from 'testcafe'; + +fixture`Click on "select" element` + .page('http://localhost:3000/fixtures/regression/gh-5616/pages/index.html'); + +test('Element "select" shouldn\'t be opened', async (t) => { + await t.click('select'); + await t.expect(Selector('option').filterVisible().count).eql(0); +}); diff --git a/test/functional/fixtures/regression/gh-5886/pages/index.html b/test/functional/fixtures/regression/gh-5886/pages/index.html new file mode 100644 index 00000000000..1481216a6c2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5886/pages/index.html @@ -0,0 +1,17 @@ + + + + + gh-5886 + + + +
+ + + diff --git a/test/functional/fixtures/regression/gh-5886/test.js b/test/functional/fixtures/regression/gh-5886/test.js new file mode 100644 index 00000000000..d7a1d29a2b3 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5886/test.js @@ -0,0 +1,9 @@ +const { skipInNativeAutomation } = require('../../../utils/skip-in'); + +describe('[Regression](GH-5886) - Selector for element after switching to the rewritten iframe', function () { + // NOTE: iframe without src, create on client side, content is added using the `write` method. + // proxy mode has some additional scripts to process this scenario + skipInNativeAutomation('Should exist', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-5886/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5886/testcafe-fixtures/index.js new file mode 100644 index 00000000000..b491ba25a8a --- /dev/null +++ b/test/functional/fixtures/regression/gh-5886/testcafe-fixtures/index.js @@ -0,0 +1,10 @@ +import { Selector } from 'testcafe'; + +fixture `Selector for element after switching to the rewritten iframe` + .page `http://localhost:3000/fixtures/regression/gh-5886/pages/index.html`; + +test('Should exist', async t => { + await t + .switchToIframe('#frame') + .expect(Selector('#test').withText('The Test Text').exists).ok(); +}); diff --git a/test/functional/fixtures/regression/gh-5921/pages/index.html b/test/functional/fixtures/regression/gh-5921/pages/index.html new file mode 100644 index 00000000000..99fe552394a --- /dev/null +++ b/test/functional/fixtures/regression/gh-5921/pages/index.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-5921/test.js b/test/functional/fixtures/regression/gh-5921/test.js new file mode 100644 index 00000000000..2d13418bba6 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5921/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-5921) typeText should replace the old value when the old value length fits maxlength', () => { + it('Should replace the old value when it fits the maxlength', () => { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-5921/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5921/testcafe-fixtures/index.js new file mode 100644 index 00000000000..2257cbc35ff --- /dev/null +++ b/test/functional/fixtures/regression/gh-5921/testcafe-fixtures/index.js @@ -0,0 +1,10 @@ +import { Selector } from 'testcafe'; + +fixture `GH-5921 - typeText should replace the old value when the old value length fits maxlength` + .page `http://localhost:3000/fixtures/regression/gh-5921/pages/index.html`; + +test(`Replace the old value`, async t => { + await t + .typeText('#input', 'cafe', { replace: true }) + .expect(Selector('#input').value).eql('cafe'); +}); diff --git a/test/functional/fixtures/regression/gh-5961/pages/index.html b/test/functional/fixtures/regression/gh-5961/pages/index.html new file mode 100644 index 00000000000..5a28b2a6e21 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5961/pages/index.html @@ -0,0 +1,73 @@ + + + + + + Testcafe screenshot sizing issue + + + + +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/test/functional/fixtures/regression/gh-5961/test.js b/test/functional/fixtures/regression/gh-5961/test.js new file mode 100644 index 00000000000..c44e02b41fc --- /dev/null +++ b/test/functional/fixtures/regression/gh-5961/test.js @@ -0,0 +1,19 @@ +const assertionHelper = require('../../../assertion-helper.js'); +const { expect } = require('chai'); +const config = require('../../../config.js'); + +if (config.useLocalBrowsers && config.useHeadlessBrowsers) { + describe('[Regression](GH-5961)', () => { + afterEach(assertionHelper.removeScreenshotDir); + + it('Screenshot', () => { + return runTests('./testcafe-fixtures/index.js', 'Take a resized full page screenshot', { setScreenshotPath: true }) + .then(function () { + return assertionHelper.checkScreenshotFileFullPage(false, 'custom'); + }) + .then(function (result) { + expect(result).eql(true); + }); + }); + }); +} diff --git a/test/functional/fixtures/regression/gh-5961/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5961/testcafe-fixtures/index.js new file mode 100644 index 00000000000..8fcaeb24fe2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5961/testcafe-fixtures/index.js @@ -0,0 +1,15 @@ +import { ClientFunction } from 'testcafe'; +import { parseUserAgent } from '../../../../../../lib/utils/parse-user-agent.js'; + +fixture`Getting Started` + .page`http://localhost:3000/fixtures/regression/gh-5961/pages/index.html`; + +test('Take a resized full page screenshot', async t => { + const ua = await ClientFunction(() => navigator.userAgent.toString())(); + + await t.resizeWindow(1024, 768); + await t.takeScreenshot({ + path: 'custom/' + parseUserAgent(ua).name + '.png', + fullPage: true, + }); +}); diff --git a/test/functional/fixtures/regression/gh-5992/pages/page1.html b/test/functional/fixtures/regression/gh-5992/pages/page1.html new file mode 100644 index 00000000000..ed074eec55f --- /dev/null +++ b/test/functional/fixtures/regression/gh-5992/pages/page1.html @@ -0,0 +1,11 @@ + + + + + Title + + +

Page 1

+ Go to page 2 + + diff --git a/test/functional/fixtures/regression/gh-5992/pages/page2.html b/test/functional/fixtures/regression/gh-5992/pages/page2.html new file mode 100644 index 00000000000..eb7c33dbc32 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5992/pages/page2.html @@ -0,0 +1,10 @@ + + + + + Title + + +

Page 2

+ + diff --git a/test/functional/fixtures/regression/gh-5992/test.js b/test/functional/fixtures/regression/gh-5992/test.js new file mode 100644 index 00000000000..5e61f9bf926 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5992/test.js @@ -0,0 +1,5 @@ +describe('Should store localStorage values between pages (GH-5992)', () => { + it('Should store localStorage values between pages (GH-5992)', () => { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-5992/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-5992/testcafe-fixtures/index.js new file mode 100644 index 00000000000..3eb0cb37e55 --- /dev/null +++ b/test/functional/fixtures/regression/gh-5992/testcafe-fixtures/index.js @@ -0,0 +1,24 @@ +import { ClientFunction } from 'testcafe'; + +fixture('Fixture') + .page('http://localhost:3000/fixtures/regression/gh-5992/page1.html'); + +const setLocalStorageItem = ClientFunction(() => { + window.localStorage.setItem('foo', 'bar'); +}); + +const getLocalStorageItem = ClientFunction(() => { + return window.localStorage.getItem('foo'); +}); + +test('test', async t => { + await setLocalStorageItem(); + + const valueOnPage1 = await getLocalStorageItem(); + + await t.navigateTo('page2.html'); + + const valueOnPage2 = await getLocalStorageItem(); + + await t.expect(valueOnPage1).eql(valueOnPage2); +}); diff --git a/test/functional/fixtures/regression/gh-6205/test.js b/test/functional/fixtures/regression/gh-6205/test.js new file mode 100644 index 00000000000..95f92a66f3e --- /dev/null +++ b/test/functional/fixtures/regression/gh-6205/test.js @@ -0,0 +1,23 @@ +const { expect } = require('chai'); + +describe('[Regression](GH-6205)', function () { + it('Stack traces should not be filtered for paths that include a folder named "testcafe-hammerhead"', function () { + return runTests( + 'testcafe-fixtures/testcafe-hammerhead/index.js', + 'Should throw an error', + { shouldFail: true, only: 'chrome' } + ).catch((errs) => { + expect(errs[0]).match(/at .*index.js.*:4/); + }); + }); + + it('Stack traces should not be filtered for paths that include a folder named "source-map-support"', function () { + return runTests( + 'testcafe-fixtures/source-map-support/index.js', + null, + { shouldFail: true, only: 'chrome' } + ).catch((err) => { + expect(err.stack).match(/at .*index.js.*:1/); + }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-6205/testcafe-fixtures/source-map-support/index.js b/test/functional/fixtures/regression/gh-6205/testcafe-fixtures/source-map-support/index.js new file mode 100644 index 00000000000..92fde1f2ba2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6205/testcafe-fixtures/source-map-support/index.js @@ -0,0 +1 @@ +throw new Error(); diff --git a/test/functional/fixtures/regression/gh-6205/testcafe-fixtures/testcafe-hammerhead/index.js b/test/functional/fixtures/regression/gh-6205/testcafe-fixtures/testcafe-hammerhead/index.js new file mode 100644 index 00000000000..4b004680588 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6205/testcafe-fixtures/testcafe-hammerhead/index.js @@ -0,0 +1,5 @@ +fixture `My Fixture`; + +test('Should throw an error', async () => { + throw new Error(); +}); diff --git a/test/functional/fixtures/regression/gh-632/test.js b/test/functional/fixtures/regression/gh-632/test.js index f2aab361821..d48d7a8ac17 100644 --- a/test/functional/fixtures/regression/gh-632/test.js +++ b/test/functional/fixtures/regression/gh-632/test.js @@ -1,5 +1,5 @@ describe('[Regression](GH-632)', function () { - it('Should not fail if window.self is overriden', function () { + it('Should not fail if window.self is overridden', function () { return runTests('./testcafe-fixtures/click-test.js', 'Click on body'); }); }); diff --git a/test/functional/fixtures/regression/gh-637/test.js b/test/functional/fixtures/regression/gh-637/test.js index 782b922bab2..716e48dfb0b 100644 --- a/test/functional/fixtures/regression/gh-637/test.js +++ b/test/functional/fixtures/regression/gh-637/test.js @@ -1,16 +1,16 @@ -var tmp = require('tmp'); -var path = require('path'); -var copy = require('recursive-copy'); +const tmp = require('tmp'); +const path = require('path'); +const copy = require('recursive-copy'); describe('[Regression](GH-637)', function () { it("Should let test file locate babel-runtime if it's not installed on global or test file node_modules lookup scope", function () { tmp.setGracefulCleanup(); - var tmpDir = tmp.dirSync().name; - var srcDir = path.join(__dirname, './data'); + const tmpDir = tmp.dirSync().name; + const srcDir = path.join(__dirname, './testcafe-fixtures'); return copy(srcDir, tmpDir).then(function () { - var testFile = path.join(tmpDir, './testfile.js'); + const testFile = path.join(tmpDir, './testfile.js'); return runTests(testFile, 'Some test', { only: 'chrome' }); }); diff --git a/test/functional/fixtures/regression/gh-637/data/testfile.js b/test/functional/fixtures/regression/gh-637/testcafe-fixtures/testfile.js similarity index 100% rename from test/functional/fixtures/regression/gh-637/data/testfile.js rename to test/functional/fixtures/regression/gh-637/testcafe-fixtures/testfile.js diff --git a/test/functional/fixtures/regression/gh-6405/pages/index.html b/test/functional/fixtures/regression/gh-6405/pages/index.html new file mode 100644 index 00000000000..e69213ffa64 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6405/pages/index.html @@ -0,0 +1,11 @@ + + + + + gh-6405 + + + + + diff --git a/test/functional/fixtures/regression/gh-6405/test.js b/test/functional/fixtures/regression/gh-6405/test.js new file mode 100644 index 00000000000..6f0eefc6454 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6405/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-6405)', function () { + it('Should not throw error on `tab` action when cross-domain iframe presents on page', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-6405/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-6405/testcafe-fixtures/index.js new file mode 100644 index 00000000000..c5e8fcc5b1c --- /dev/null +++ b/test/functional/fixtures/regression/gh-6405/testcafe-fixtures/index.js @@ -0,0 +1,6 @@ +fixture `Should not throw error on \`tab\` action when cross-domain iframe presents on page` + .page `http://localhost:3000/fixtures/regression/gh-6405/pages/index.html`; + +test(`press tab`, async t => { + await t.pressKey('tab'); +}); diff --git a/test/functional/fixtures/regression/gh-6563/pages/index.html b/test/functional/fixtures/regression/gh-6563/pages/index.html new file mode 100644 index 00000000000..991014494c2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6563/pages/index.html @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-6563/test.js b/test/functional/fixtures/regression/gh-6563/test.js new file mode 100644 index 00000000000..b90960754d9 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6563/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-6563)', function () { + it('Should pass correct "this" argument on the "beforeunload" event', function () { + return runTests('testcafe-fixtures/index.js', null, { only: 'chrome' }); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-6563/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-6563/testcafe-fixtures/index.js new file mode 100644 index 00000000000..d8decbc96e6 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6563/testcafe-fixtures/index.js @@ -0,0 +1,10 @@ +import { ClientFunction } from 'testcafe'; + +const reload = ClientFunction(() => window.location.reload()); + +fixture `Should pass correct "this" argument on the "beforeunload" event` + .page `http://localhost:3000/fixtures/regression/gh-6563/pages/index.html`; + +test(`Should pass correct "this" argument on the "beforeunload" event`, async () => { + await reload(); +}); diff --git a/test/functional/fixtures/regression/gh-6646/pages/index.html b/test/functional/fixtures/regression/gh-6646/pages/index.html new file mode 100644 index 00000000000..e8e3253f50c --- /dev/null +++ b/test/functional/fixtures/regression/gh-6646/pages/index.html @@ -0,0 +1,9 @@ + + + + + gh-6646 + + + + diff --git a/test/functional/fixtures/regression/gh-6646/test.js b/test/functional/fixtures/regression/gh-6646/test.js new file mode 100644 index 00000000000..e0a9ae62302 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6646/test.js @@ -0,0 +1,38 @@ +const expect = require('chai').expect; + +const config = require('../../../config'); +const { createReporter } = require('../../../utils/reporter'); + +let testErrors = null; +const actionErrors = []; + +const reporter = createReporter({ + reportTestActionDone (name, { err }) { + actionErrors.push(err); + }, + reportTestDone (name, testRunInfo) { + testErrors = testRunInfo.errs; + }, +}); + +// TODO: Stabilize test on macOS +(config.hasBrowser('safari') ? describe.skip : describe)('Should pass the "error.id" argument to the reporter', function () { + it('Action error', function () { + return runTests('./testcafe-fixtures/index.js', 'Action error', { + reporter, + selectorTimeout: 100, + }) + .then(function () { + expect(testErrors.length).eql(config.browsers.length); + expect(testErrors.length).eql(actionErrors.length); + + for (const err of testErrors) { + expect(!!err.id).eql(true); + expect(!!testErrors.find(e => e.id === err.id)).eql(true); + } + }); + }); + +}); + + diff --git a/test/functional/fixtures/regression/gh-6646/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-6646/testcafe-fixtures/index.js new file mode 100644 index 00000000000..18e539941ff --- /dev/null +++ b/test/functional/fixtures/regression/gh-6646/testcafe-fixtures/index.js @@ -0,0 +1,6 @@ +fixture `Should pass the "error.id" argument to the reporter` + .page `http://localhost:3000/fixtures/regression/gh-6646/pages/index.html`; + +test(`Action error`, async t => { + await t.click('non-existing-element'); +}); diff --git a/test/functional/fixtures/regression/gh-6722/constants.js b/test/functional/fixtures/regression/gh-6722/constants.js new file mode 100644 index 00000000000..29cdbc57b42 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6722/constants.js @@ -0,0 +1,42 @@ +const ERRORS = { + Client: 'E1', + Server: 'E2', + None: '', +}; + +const SUCCESS_RESULT_ATTEMPTS = [ + ERRORS.Server, + ERRORS.Client, + ERRORS.None, + ERRORS.None, + ERRORS.None, +]; + +const FAIL_RESULT_ATTEMPTS = [ + ERRORS.Server, + ERRORS.Client, + ERRORS.None, + ERRORS.None, + ERRORS.Server, +]; + +const Counter = function () { + this.counters = {}; + + this.add = alias => { + if (!this.counters.hasOwnProperty(alias)) + this.counters[alias] = -1; + + this.counters[alias]++; + }; + this.get = alias => { + return this.counters[alias]; + }; +}; + +module.exports = { + ERRORS, + SUCCESS_RESULT_ATTEMPTS, + FAIL_RESULT_ATTEMPTS, + Counter, +}; diff --git a/test/functional/fixtures/regression/gh-6722/pages/index.html b/test/functional/fixtures/regression/gh-6722/pages/index.html new file mode 100644 index 00000000000..7023fe0eea2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6722/pages/index.html @@ -0,0 +1,20 @@ + + + + + gh-6722 + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-6722/test.js b/test/functional/fixtures/regression/gh-6722/test.js new file mode 100644 index 00000000000..66964cf79dd --- /dev/null +++ b/test/functional/fixtures/regression/gh-6722/test.js @@ -0,0 +1,75 @@ +const expect = require('chai').expect; +const { SUCCESS_RESULT_ATTEMPTS, FAIL_RESULT_ATTEMPTS, ERRORS } = require('./constants'); +const { createReporter } = require('../../../utils/reporter'); + +const getCustomReporter = function (result) { + return createReporter({ + reportTestDone: function (name, { errs, quarantine, browsers }) { + if (quarantine) + Object.assign(result, { quarantine, browsers }); + + if (errs && errs.length > 0) + throw new Error(errs[0]['message']); + }, + }); +}; + +const expectAttempts = (attempts, { quarantine, browsers }) => { + const attemptsCount = attempts.length; + const successAttemptsCount = attempts.filter(attempt => attempt === ERRORS.None).length; + const failedAttemptsCount = attempts.filter(attempt => attempt !== ERRORS.None).length; + const browsersCount = browsers.length; + + const currentAttemptsCount = Object.keys(quarantine).length; + const currentSuccessAttemptsCount = Object.values(quarantine).filter(attempt => attempt.passed).length; + const currentFailedAttemptsCount = Object.values(quarantine).filter(attempt => !attempt.passed).length; + const currentTestRunIdsCount = browsers.reduce((counter, browser) => counter + browser.quarantineAttemptsTestRunIds.length, 0); + + // We have to add the number of attempts per browser as we also kept the old behavior. {1:{}, 2:{}, ... testRunId1:{}, testRunId2:{}, ...} + const expectedAttemptsCount = attemptsCount * (browsersCount + 1); + const expectedSuccessAttemptsCount = successAttemptsCount * (browsersCount + 1); + const expectedFailedAttemptsCount = failedAttemptsCount * (browsersCount + 1); + + expect(currentAttemptsCount).to.equal(expectedAttemptsCount); + expect(currentSuccessAttemptsCount).to.equal(expectedSuccessAttemptsCount); + expect(currentFailedAttemptsCount).to.equal(expectedFailedAttemptsCount); + expect(currentTestRunIdsCount).to.equal(attemptsCount * browsersCount); +}; + +describe('[Regression](GH-6722)', function () { + it('Should success run with three success and two fail attempts', function () { + const result = {}; + const reporter = [getCustomReporter(result)]; + + return runTests('./testcafe-fixtures/index.js', 'Throw exceptions on two attempts', { + reporter, + skipJsErrors: false, + quarantineMode: true, + }).then(() => { + expectAttempts(SUCCESS_RESULT_ATTEMPTS, result); + }).catch((err)=>{ + throw new Error(err.message); + }); + }); + + it('Should fail with two success and three fail attempts', function () { + const result = {}; + const reporter = [getCustomReporter(result)]; + const SHOULD_FAIL_ERROR = 'Test should fail'; + + return runTests('./testcafe-fixtures/index.js', 'Throw exceptions on three attempts', { + quarantineMode: true, + shouldFail: true, + skipJsErrors: false, + reporter, + }).then(() => { + throw new Error(SHOULD_FAIL_ERROR); + }).catch((err) => { + if (err && err.message === SHOULD_FAIL_ERROR) + throw new Error(SHOULD_FAIL_ERROR); + + expectAttempts(FAIL_RESULT_ATTEMPTS, result); + }); + }); + +}); diff --git a/test/functional/fixtures/regression/gh-6722/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-6722/testcafe-fixtures/index.js new file mode 100644 index 00000000000..a8dd62c2b27 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6722/testcafe-fixtures/index.js @@ -0,0 +1,35 @@ +import { + FAIL_RESULT_ATTEMPTS, + SUCCESS_RESULT_ATTEMPTS, + ERRORS, + Counter, +} from '../constants.js'; + +const counter = new Counter(); + +const processAttempt = async (attempts, testNumber, t) => { + if (attempts[testNumber] === ERRORS.Server) + throw new Error(`Custom server exception on test #${ testNumber }`); + + if (attempts[testNumber] === ERRORS.Client) { + await t + .typeText('#testInput', `Custom client exception on test #${ testNumber }`) + .click('#failButton'); + } +}; + +fixture`GH-6722 - Provide more information about errors for each test run in quarantine mode` + .page`http://localhost:3000/fixtures/regression/gh-6722/pages/index.html`; + +test(`Throw exceptions on two attempts`, async t => { + counter.add(t.browser.alias); + + await processAttempt(SUCCESS_RESULT_ATTEMPTS, counter.get(t.browser.alias), t); +}); + + +test(`Throw exceptions on three attempts`, async t => { + counter.add(t.browser.alias); + + await processAttempt(FAIL_RESULT_ATTEMPTS, counter.get(t.browser.alias), t); +}); diff --git a/test/functional/fixtures/regression/gh-6949/pages/with-button.html b/test/functional/fixtures/regression/gh-6949/pages/with-button.html new file mode 100644 index 00000000000..13ab22f7f75 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/pages/with-button.html @@ -0,0 +1,16 @@ + + + + + Title + + +
+ + +
+ + diff --git a/test/functional/fixtures/regression/gh-6949/pages/with-disabled-checkbox.html b/test/functional/fixtures/regression/gh-6949/pages/with-disabled-checkbox.html new file mode 100644 index 00000000000..7beeb3231b6 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/pages/with-disabled-checkbox.html @@ -0,0 +1,15 @@ + + + + + Title + + +
+ + +
+ + diff --git a/test/functional/fixtures/regression/gh-6949/pages/with-div.html b/test/functional/fixtures/regression/gh-6949/pages/with-div.html new file mode 100644 index 00000000000..3c42a80d1f5 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/pages/with-div.html @@ -0,0 +1,18 @@ + + + + + Title + + +
+ + +
+ + diff --git a/test/functional/fixtures/regression/gh-6949/pages/with-link-without-href.html b/test/functional/fixtures/regression/gh-6949/pages/with-link-without-href.html new file mode 100644 index 00000000000..703bb57a635 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/pages/with-link-without-href.html @@ -0,0 +1,18 @@ + + + + + Title + + +
+ + +
+ + diff --git a/test/functional/fixtures/regression/gh-6949/pages/with-link.html b/test/functional/fixtures/regression/gh-6949/pages/with-link.html new file mode 100644 index 00000000000..41b50808340 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/pages/with-link.html @@ -0,0 +1,18 @@ + + + + + Title + + +
+ + +
+ + diff --git a/test/functional/fixtures/regression/gh-6949/test.js b/test/functional/fixtures/regression/gh-6949/test.js new file mode 100644 index 00000000000..6fd48b0d50f --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/test.js @@ -0,0 +1,25 @@ +describe('[Regression](GH-6949)', () => { + it('Should change checkbox state when clicking checkbox label', () => { + return runTests('./testcafe-fixtures/with-link.js', 'Click checkbox label'); + }); + + it('Should NOT change checkbox state when clicking a LINK inside the checkbox label', () => { + return runTests('./testcafe-fixtures/with-link.js', 'Click link inside checkbox label'); + }); + + it('Should change checkbox state when clicking a LINK without href attribute inside the checkbox label', () => { + return runTests('./testcafe-fixtures/with-link-without-href.js', 'Click link without href inside checkbox label'); + }); + + it('Should NOT change checkbox state when clicking a BUTTON inside the checkbox label', () => { + return runTests('./testcafe-fixtures/with-button.js', 'Click button inside checkbox label'); + }); + + it('Should change checkbox state when clicking a DIV with onclick handler inside the checkbox label', () => { + return runTests('./testcafe-fixtures/with-div.js', 'Click div inside checkbox label'); + }); + + it('Should NOT change checkbox state when clicking the label of the disabled checkbox', () => { + return runTests('./testcafe-fixtures/with-disabled-checkbox.js', 'Click disabled checkbox label'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-button.js b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-button.js new file mode 100644 index 00000000000..609e83eb630 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-button.js @@ -0,0 +1,12 @@ +import { Selector } from 'testcafe'; + +fixture`Getting Started` + .page`http://localhost:3000/fixtures/regression/gh-6949/pages/with-button.html`; + +test('Click button inside checkbox label', async t => { + const button = Selector('#clickable-element'); + const checkBox = Selector('#checkbox'); + + await t.click(button); + await t.expect(checkBox.checked).eql(false); +}); diff --git a/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-disabled-checkbox.js b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-disabled-checkbox.js new file mode 100644 index 00000000000..668858b75ac --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-disabled-checkbox.js @@ -0,0 +1,12 @@ +import { Selector } from 'testcafe'; + +fixture`Getting Started` + .page`http://localhost:3000/fixtures/regression/gh-6949/pages/with-disabled-checkbox.html`; + +test('Click disabled checkbox label', async t => { + const label = Selector('#checkbox-label'); + const checkBox = Selector('#checkbox'); + + await t.click(label); + await t.expect(checkBox.checked).eql(false); +}); diff --git a/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-div.js b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-div.js new file mode 100644 index 00000000000..745ebdb259c --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-div.js @@ -0,0 +1,12 @@ +import { Selector } from 'testcafe'; + +fixture`Getting Started` + .page`http://localhost:3000/fixtures/regression/gh-6949/pages/with-div.html`; + +test('Click div inside checkbox label', async t => { + const div = Selector('#clickable-element'); + const checkBox = Selector('#checkbox'); + + await t.click(div); + await t.expect(checkBox.checked).eql(true); +}); diff --git a/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-link-without-href.js b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-link-without-href.js new file mode 100644 index 00000000000..529e447447c --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-link-without-href.js @@ -0,0 +1,12 @@ +import { Selector } from 'testcafe'; + +fixture`Getting Started` + .page`http://localhost:3000/fixtures/regression/gh-6949/pages/with-link-without-href.html`; + +test('Click link without href inside checkbox label', async t => { + const link = Selector('#clickable-element'); + const checkBox = Selector('#checkbox'); + + await t.click(link); + await t.expect(checkBox.checked).eql(true); +}); diff --git a/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-link.js b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-link.js new file mode 100644 index 00000000000..bb4440aebaf --- /dev/null +++ b/test/functional/fixtures/regression/gh-6949/testcafe-fixtures/with-link.js @@ -0,0 +1,20 @@ +import { Selector } from 'testcafe'; + +fixture`Getting Started` + .page`http://localhost:3000/fixtures/regression/gh-6949/pages/with-link.html`; + +test('Click checkbox label', async t => { + const label = Selector('#checkbox-label'); + const checkBox = Selector('#checkbox'); + + await t.click(label); + await t.expect(checkBox.checked).eql(true); +}); + +test('Click link inside checkbox label', async t => { + const link = Selector('#clickable-element'); + const checkBox = Selector('#checkbox'); + + await t.click(link); + await t.expect(checkBox.checked).eql(false); +}); diff --git a/test/functional/fixtures/regression/gh-6969/pages/index.html b/test/functional/fixtures/regression/gh-6969/pages/index.html new file mode 100644 index 00000000000..e26fe1e05c4 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6969/pages/index.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + diff --git a/test/functional/fixtures/regression/gh-6969/test.js b/test/functional/fixtures/regression/gh-6969/test.js new file mode 100644 index 00000000000..6495f34d8e9 --- /dev/null +++ b/test/functional/fixtures/regression/gh-6969/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-6969)', () => { + it('Should change checkbox checked state when pressing space key', () => { + return runTests('./testcafe-fixtures/index.js', 'Should change checkbox checked state when pressing space key'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-6969/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-6969/testcafe-fixtures/index.js new file mode 100644 index 00000000000..c67b372c02c --- /dev/null +++ b/test/functional/fixtures/regression/gh-6969/testcafe-fixtures/index.js @@ -0,0 +1,15 @@ +import { Selector } from 'testcafe'; + +fixture`Getting Started` + .page`http://localhost:3000/fixtures/regression/gh-6969/pages/index.html`; + +test('Should change checkbox checked state when pressing space key', async t => { + const checkBox = Selector('#checkbox'); + + await t.click(checkBox) + .expect(checkBox.checked).ok() + .pressKey('space') + .expect(checkBox.checked).notOk() + .pressKey('space') + .expect(checkBox.checked).ok(); +}); diff --git a/test/functional/fixtures/regression/gh-6998/pages/index.html b/test/functional/fixtures/regression/gh-6998/pages/index.html new file mode 100644 index 00000000000..214f83c6b9b --- /dev/null +++ b/test/functional/fixtures/regression/gh-6998/pages/index.html @@ -0,0 +1,9 @@ + + +
+ + + +
+ + diff --git a/test/functional/fixtures/regression/gh-6998/test.js b/test/functional/fixtures/regression/gh-6998/test.js new file mode 100644 index 00000000000..11ae6adcc4e --- /dev/null +++ b/test/functional/fixtures/regression/gh-6998/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-6998)', () => { + it('SVG visibility check in case of a parent with nullable dimensions', async () => { + return runTests('./testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-6998/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-6998/testcafe-fixtures/index.js new file mode 100644 index 00000000000..521080881bb --- /dev/null +++ b/test/functional/fixtures/regression/gh-6998/testcafe-fixtures/index.js @@ -0,0 +1,6 @@ +fixture `Fixture` + .page('../pages/index.html'); + +test('test', async t => { + await t.click('svg'); +}); diff --git a/test/functional/fixtures/regression/gh-7097/pages/index.html b/test/functional/fixtures/regression/gh-7097/pages/index.html new file mode 100644 index 00000000000..0f4e56b7365 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7097/pages/index.html @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/test/functional/fixtures/regression/gh-7097/test.js b/test/functional/fixtures/regression/gh-7097/test.js new file mode 100644 index 00000000000..61bf38ef8a1 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7097/test.js @@ -0,0 +1,9 @@ +const config = require('../../../config'); + +if (!config.nativeAutomation) { + describe('[Regression](GH-7097) - Should fail fetch on non exist path with disableNativeAutomation', function () { + it('Fetch failed', function () { + return runTests('testcafe-fixtures/fetch-to-non-exist-path.js'); + }); + }); +} diff --git a/test/functional/fixtures/regression/gh-7097/testcafe-fixtures/fetch-to-non-exist-path.js b/test/functional/fixtures/regression/gh-7097/testcafe-fixtures/fetch-to-non-exist-path.js new file mode 100644 index 00000000000..63d1dd0285e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7097/testcafe-fixtures/fetch-to-non-exist-path.js @@ -0,0 +1,13 @@ +import { ClientFunction } from 'testcafe'; + +const getFailedFetchWindow = ClientFunction(() => window.failedFetch); + +fixture `GH-7097 - Should fail fetch on non exist path with disableNativeAutomation`; + +test(`fail fetch on non exist path with disableNativeAutomation`, async t => { + await t.navigateTo('http://localhost:3000/fixtures/regression/gh-7097/pages/index.html'); + + await t.wait(100); // We wait when fench failed on client + + await t.expect(await getFailedFetchWindow()).eql(true); +}); diff --git a/test/functional/fixtures/regression/gh-711/testcafe-fixtures/typing-in-content-editable-test.js b/test/functional/fixtures/regression/gh-711/testcafe-fixtures/typing-in-content-editable-test.js index 41b818e188d..c0faab9f01c 100644 --- a/test/functional/fixtures/regression/gh-711/testcafe-fixtures/typing-in-content-editable-test.js +++ b/test/functional/fixtures/regression/gh-711/testcafe-fixtures/typing-in-content-editable-test.js @@ -6,8 +6,8 @@ fixture `GH-711` .page `http://localhost:3000/fixtures/regression/gh-711/pages/index.html`; -var getBodyPlainText = ClientFunction(() => { - var plainTextRE = /\s+|\n|\r/g; +const getBodyPlainText = ClientFunction(() => { + const plainTextRE = /\s+|\n|\r/g; return document.body.textContent.replace(plainTextRE, ''); }); @@ -22,7 +22,7 @@ test('Typing in contentEditable body', async t => { .click('body') .typeText('body', 'test'); - var actualText = await getBodyPlainText(); + const actualText = await getBodyPlainText(); expect(actualText.indexOf('test') !== -1).to.be.ok; }); @@ -32,8 +32,8 @@ test('Typing in contentEditable body with not-contentEditable children', async t .selectText('body') .typeText('body', 'test'); - var actualText = await getBodyPlainText(); - var expectedText = 'div1testdiv3'; + const actualText = await getBodyPlainText(); + const expectedText = 'div1testdiv3'; expect(actualText.indexOf(expectedText) !== -1).to.be.ok; }); diff --git a/test/functional/fixtures/regression/gh-7377/pages/index.html b/test/functional/fixtures/regression/gh-7377/pages/index.html new file mode 100644 index 00000000000..5f8c11b5d6f --- /dev/null +++ b/test/functional/fixtures/regression/gh-7377/pages/index.html @@ -0,0 +1,35 @@ + + + + + + + +
+
+
Section A
+

+
+
+
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/test/functional/fixtures/regression/gh-7377/test.js b/test/functional/fixtures/regression/gh-7377/test.js new file mode 100644 index 00000000000..c75bb8b79df --- /dev/null +++ b/test/functional/fixtures/regression/gh-7377/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-7377)', () => { + it('Should scroll element higher than sticky footer and type text in element', async () => { + return runTests('./testcafe-fixtures/'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7377/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7377/testcafe-fixtures/index.js new file mode 100644 index 00000000000..e90db859274 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7377/testcafe-fixtures/index.js @@ -0,0 +1,12 @@ +import { Selector } from 'testcafe'; + +fixture `fixture` + .page `http://localhost:3000/fixtures/regression/gh-7377/pages/index.html`; + +const input = Selector('#input').find('input'); + + +test('Should scroll to the input and enter text into it', async (t) => { + await t.typeText(input, 'test'); + await t.expect(input.value).eql('test'); +}); diff --git a/test/functional/fixtures/regression/gh-743/testcafe-fixtures/two-tests-in-a-fixture.js b/test/functional/fixtures/regression/gh-743/testcafe-fixtures/two-tests-in-a-fixture.js index 197355952a1..8ec91b6d54a 100644 --- a/test/functional/fixtures/regression/gh-743/testcafe-fixtures/two-tests-in-a-fixture.js +++ b/test/functional/fixtures/regression/gh-743/testcafe-fixtures/two-tests-in-a-fixture.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; fixture `GH-743`; -var secondStarted = false; +let secondStarted = false; test('First test', async t => { await t.wait(100); diff --git a/test/functional/fixtures/regression/gh-7454/pages/index.html b/test/functional/fixtures/regression/gh-7454/pages/index.html new file mode 100644 index 00000000000..dd113a71442 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7454/pages/index.html @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/test/functional/fixtures/regression/gh-7454/test.js b/test/functional/fixtures/regression/gh-7454/test.js new file mode 100644 index 00000000000..dbd77d623bd --- /dev/null +++ b/test/functional/fixtures/regression/gh-7454/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-7387)', () => { + it('Should click on SVG inside shadowRoot', async () => { + return runTests('./testcafe-fixtures/index.js', ''); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7454/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7454/testcafe-fixtures/index.js new file mode 100644 index 00000000000..8db15a48750 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7454/testcafe-fixtures/index.js @@ -0,0 +1,9 @@ +import { Selector } from 'testcafe'; + +fixture('Fixture').page('../pages/index.html'); + +test('Test', async t => { + const rectElement = Selector('div').shadowRoot().find('rect'); + + await t.click(rectElement); +}); diff --git a/test/functional/fixtures/regression/gh-7482/test.js b/test/functional/fixtures/regression/gh-7482/test.js new file mode 100644 index 00000000000..e0259ae3233 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7482/test.js @@ -0,0 +1,5 @@ +describe('[Regression](GH-7482)', () => { + it("Shouldn't throw errors if test and fixtures methods are executed before test and fixtures", async () => { + return runTests('./testcafe-fixtures/index.js', '', { only: ['chrome'] }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7482/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7482/testcafe-fixtures/index.js new file mode 100644 index 00000000000..889039db5d8 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7482/testcafe-fixtures/index.js @@ -0,0 +1,5 @@ +fixture.meta('dev', 'true')('Fixture'); + +test.meta('dev', 'true')('Test', async t => { + await t.expect(true).ok(); +}); diff --git a/test/functional/fixtures/regression/gh-7483/pages/index.html b/test/functional/fixtures/regression/gh-7483/pages/index.html new file mode 100644 index 00000000000..2dfd75b28b6 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7483/pages/index.html @@ -0,0 +1,23 @@ + + + + + + + + + +
+ + + diff --git a/test/functional/fixtures/regression/gh-7483/test.js b/test/functional/fixtures/regression/gh-7483/test.js new file mode 100644 index 00000000000..3e7bc59d79e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7483/test.js @@ -0,0 +1,9 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('[Regression](GH-7483)', function () { + onlyInNativeAutomation('Should click on the element if the element is behind the Status Bar', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7483/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7483/testcafe-fixtures/index.js new file mode 100644 index 00000000000..ff51a3ecd38 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7483/testcafe-fixtures/index.js @@ -0,0 +1,10 @@ +import { Selector } from 'testcafe'; + +fixture `Should click on the element if the element is behind the Status Bar` + .page('../pages/index.html'); + +test('Click the button behind the StatusBar', async t => { + await t.click('button'); + + await t.expect(Selector('#logger').innerText).eql('OK'); +}); diff --git a/test/functional/fixtures/regression/gh-751/pages/index.html b/test/functional/fixtures/regression/gh-751/pages/index.html index 51d5a3ab9d9..e030469bcab 100644 --- a/test/functional/fixtures/regression/gh-751/pages/index.html +++ b/test/functional/fixtures/regression/gh-751/pages/index.html @@ -15,7 +15,7 @@ window.dblclickEvents.push(Date.now()); } - var dblclickBtn = document.getElementById('dblclick'); + const dblclickBtn = document.getElementById('dblclick'); dblclickBtn.addEventListener('mouseup', logDblclickPerformance); dblclickBtn.addEventListener('click', logDblclickPerformance); @@ -31,13 +31,13 @@ } function doHardWork () { - var startTime = Date.now(); + const startTime = Date.now(); while (Date.now() < startTime + window.HARD_WORK_TIME) { } } - var hardWorkMousedownBtn = document.getElementById('hardWorkMousedown'); + const hardWorkMousedownBtn = document.getElementById('hardWorkMousedown'); hardWorkMousedownBtn.addEventListener('mousedown', logClickPerformance); hardWorkMousedownBtn.addEventListener('mouseup', logClickPerformance); diff --git a/test/functional/fixtures/regression/gh-751/testcafe-fixtures/index-test.js b/test/functional/fixtures/regression/gh-751/testcafe-fixtures/index-test.js index 22a82ebceb7..3e110e84e65 100644 --- a/test/functional/fixtures/regression/gh-751/testcafe-fixtures/index-test.js +++ b/test/functional/fixtures/regression/gh-751/testcafe-fixtures/index-test.js @@ -8,12 +8,13 @@ fixture `GH-751` test('Test dblclick performance', async t => { await t.doubleClick('#dblclick'); - var dblclickPerformanceLog = await ClientFunction(() => window.dblclickEvents)(); - var firstMouseupTime = null; - var firstClickTime = null; - var secondMouseupTime = null; - var secondClickTime = null; - var dblclickTime = null; + const dblclickPerformanceLog = await ClientFunction(() => window.dblclickEvents)(); + + let firstMouseupTime = null; + let firstClickTime = null; + let secondMouseupTime = null; + let secondClickTime = null; + let dblclickTime = null; [firstMouseupTime, firstClickTime, secondMouseupTime, secondClickTime, dblclickTime] = dblclickPerformanceLog; @@ -27,7 +28,7 @@ test('Test click performance with hard work', async t => { await t.click('#hardWorkMousedown'); - var [mousedownTime, mouseupTime] = await ClientFunction(() => window.clickEvents)(); + const [mousedownTime, mouseupTime] = await ClientFunction(() => window.clickEvents)(); - expect(mouseupTime - mousedownTime).is.most(HARD_WORK_TIME + 30); + expect(mouseupTime - mousedownTime).is.most(HARD_WORK_TIME + 40); }); diff --git a/test/functional/fixtures/regression/gh-7529/test.js b/test/functional/fixtures/regression/gh-7529/test.js new file mode 100644 index 00000000000..436ff173657 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7529/test.js @@ -0,0 +1,7 @@ +const { onlyDescribeInNativeAutomation } = require('../../../utils/skip-in'); + +onlyDescribeInNativeAutomation('[Regression](GH-7529)', function () { + it('In Native Automation mode, the page should be decoded as in proxy mode', function () { + return runTests('./testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7529/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7529/testcafe-fixtures/index.js new file mode 100644 index 00000000000..5dfcc8bf5b0 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7529/testcafe-fixtures/index.js @@ -0,0 +1,10 @@ +import { Selector } from 'testcafe'; + +fixture `Regression GH-7529` + .page `http://localhost:3000/fixtures/regression/gh-7529/`; + +test('Decode page in native automation mode', async t => { + const title = await Selector('h1').textContent; + + await t.expect(title).eql('codage réussi'); +}); diff --git a/test/functional/fixtures/regression/gh-7557/pages/frame.html b/test/functional/fixtures/regression/gh-7557/pages/frame.html new file mode 100644 index 00000000000..a564ed7447e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/pages/frame.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7557/pages/index.html b/test/functional/fixtures/regression/gh-7557/pages/index.html new file mode 100644 index 00000000000..00206503b56 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/pages/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7557/pages/nested-frame.html b/test/functional/fixtures/regression/gh-7557/pages/nested-frame.html new file mode 100644 index 00000000000..802c5ddd4db --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/pages/nested-frame.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7557/test.js b/test/functional/fixtures/regression/gh-7557/test.js new file mode 100644 index 00000000000..69b6e4a2bfc --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/test.js @@ -0,0 +1,9 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('[Regression](GH-7557)', function () { + onlyInNativeAutomation('Should consider document scroll in CDP clicking', function () { + return runTests('./testcafe-fixtures/index.js', null, { only: 'chrome' }); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7557/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7557/testcafe-fixtures/index.js new file mode 100644 index 00000000000..629dbea375e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7557/testcafe-fixtures/index.js @@ -0,0 +1,24 @@ +fixture `Should consider document scroll in CDP clicking` + .page `http://localhost:3000/fixtures/regression/gh-7557/pages/index.html`; + +test('Should consider document scroll in CDP clicking', async t => { + await t.click('button'); + + const isClickedInParent = await t.eval(() => window.clickedInParent); + + await t.expect(isClickedInParent).eql(true); + + await t.switchToIframe('iframe'); + await t.click('button'); + + const isClickedInChild = await t.eval(() => window.clickedInChild); + + await t.expect(isClickedInChild).eql(true); + + await t.switchToIframe('iframe'); + await t.click('button'); + + const isClickedInNestedChild = await t.eval(() => window.clickedInNestedChild); + + await t.expect(isClickedInNestedChild).eql(true); +}); diff --git a/test/functional/fixtures/regression/gh-7566/pages/index.html b/test/functional/fixtures/regression/gh-7566/pages/index.html new file mode 100644 index 00000000000..ef8c4c479cf --- /dev/null +++ b/test/functional/fixtures/regression/gh-7566/pages/index.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7566/test.js b/test/functional/fixtures/regression/gh-7566/test.js new file mode 100644 index 00000000000..8e2754fcf06 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7566/test.js @@ -0,0 +1,9 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('[Regression](GH-7566)', function () { + onlyInNativeAutomation('Should has the "which" property equal to "0" for move events', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7566/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7566/testcafe-fixtures/index.js new file mode 100644 index 00000000000..a2c1786944e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7566/testcafe-fixtures/index.js @@ -0,0 +1,21 @@ +fixture `Should has the 'which' property equal to '0' for move events` + .page `http://localhost:3000/fixtures/regression/gh-7566/pages/index.html`; + +test(`Should has the "which" property equal to "0" for move events`, async t => { + await t.click('#btn1'); + await t.click('#btn2'); + + const log = await t.eval(() => Object.keys(window.log)); + + await t.expect(log).eql([ + 'mouseover:0', + 'mouseenter:0', + 'mousemove:0', + 'mousedown:1', + 'mouseup:1', + 'click:1', + 'mouseout:0', + 'mouseleave:0', + ]); +}); + diff --git a/test/functional/fixtures/regression/gh-7575/pages/index.html b/test/functional/fixtures/regression/gh-7575/pages/index.html new file mode 100644 index 00000000000..51091bf2c27 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7575/pages/index.html @@ -0,0 +1,24 @@ + + + + + gh-7575 + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7575/test.js b/test/functional/fixtures/regression/gh-7575/test.js new file mode 100644 index 00000000000..de69e552a06 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7575/test.js @@ -0,0 +1,9 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('[Regression](GH-7575)', function () { + onlyInNativeAutomation('Authorization header should not be modified in the native automation mode', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7575/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7575/testcafe-fixtures/index.js new file mode 100644 index 00000000000..4f6ae691c7c --- /dev/null +++ b/test/functional/fixtures/regression/gh-7575/testcafe-fixtures/index.js @@ -0,0 +1,18 @@ +import { RequestLogger } from 'testcafe'; + +const logger = RequestLogger(void 0, { + logRequestHeaders: true, +}); + +fixture `GH-7575 - Authorization header should not be modified in the native automation mode` + .page `http://localhost:3000/fixtures/regression/gh-7575/pages/index.html`; + +test(`Send XHR`, async t => { + await t.addRequestHooks(logger); + await t.click('button'); + + const header = logger.requests[0].request.headers['authorization']; + + await t.expect(header).eql('test'); +}); + diff --git a/test/functional/fixtures/regression/gh-7588/pages/index.html b/test/functional/fixtures/regression/gh-7588/pages/index.html new file mode 100644 index 00000000000..00feb9c8d16 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7588/pages/index.html @@ -0,0 +1,20 @@ + + + + + gh-7588 + + + + + + diff --git a/test/functional/fixtures/regression/gh-7588/test.js b/test/functional/fixtures/regression/gh-7588/test.js new file mode 100644 index 00000000000..882465a6d45 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7588/test.js @@ -0,0 +1,7 @@ +describe('Request body encoding in Native Automation in RequestLogger', function () { + it('Request body encoding in Native Automation in RequestLogger', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7588/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7588/testcafe-fixtures/index.js new file mode 100644 index 00000000000..d4f9df172f5 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7588/testcafe-fixtures/index.js @@ -0,0 +1,19 @@ +import { RequestLogger } from 'testcafe'; + +const logger = RequestLogger( + { url: /xhr/, method: 'POST' }, + { + logRequestBody: true, + stringifyRequestBody: true, + } +); + +fixture('Request body encoding in Native Automation in RequestLogger') + .page('http://localhost:3000/fixtures/regression/gh-7588/pages/index.html'); + +test.requestHooks(logger)('Request body encoding in Native Automation in RequestLogger', async t => { + await t.click('button'); + + await t.expect(logger.requests[0].request.body).eql('{"key":"value"}'); + +}); diff --git a/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/_child-page.html b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/_child-page.html new file mode 100644 index 00000000000..c95e50b8a46 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/_child-page.html @@ -0,0 +1,10 @@ + + + + + Child page + + +

Child page

+ + diff --git a/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/click-on-link.html b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/click-on-link.html new file mode 100644 index 00000000000..0d93476df03 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/click-on-link.html @@ -0,0 +1,11 @@ + + + + + Click on link + + +

Click on link

+ Link + + diff --git a/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/form-submit.html b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/form-submit.html new file mode 100644 index 00000000000..d4d797fd8f0 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/form-submit.html @@ -0,0 +1,14 @@ + + + + + Form submit + + +

Form submit

+
+ + +
+ + diff --git a/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/window-open.html b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/window-open.html new file mode 100644 index 00000000000..d0bc1f87ef9 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/pages/disable-multiple-windows/window-open.html @@ -0,0 +1,11 @@ + + + + + window.open + + +

Window open

+ + + diff --git a/test/functional/fixtures/regression/gh-7599/pages/index.html b/test/functional/fixtures/regression/gh-7599/pages/index.html new file mode 100644 index 00000000000..4d695baeb31 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/pages/index.html @@ -0,0 +1,10 @@ + + + + + gh-7599 + + + click me + + diff --git a/test/functional/fixtures/regression/gh-7599/test.js b/test/functional/fixtures/regression/gh-7599/test.js new file mode 100644 index 00000000000..268adcbb08b --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/test.js @@ -0,0 +1,38 @@ +const { expect } = require('chai'); +const { onlyDescribeInNativeAutomation } = require('../../../utils/skip-in'); + + +onlyDescribeInNativeAutomation('Multiple windows in the Native Automation mode', function () { + it('Should fail in the Native Automation mode if window opened using the link click', function () { + return runTests('./testcafe-fixtures/index.js', 'Should fail on link[target="blank"] click', { shouldFail: true }) + .catch(errs => { + expect(errs[0]).contains('The Native Automation mode does not support the use of multiple browser windows. Use the "disable native automation" option to continue'); + }); + }); + + it('Should fail in the Native Automation mode', function () { + return runTests('./testcafe-fixtures/index.js', 'Should fail on Multiple Window API call in the Native Automation mode', { shouldFail: true }) + .catch(errs => { + expect(errs[0]).contains('The Native Automation mode does not support the use of multiple browser windows. Use the "disable native automation" option to continue'); + }); + }); + + describe('disableMultipleWindows', function () { + const TEST_RUN_OPTIONS = { + disableMultipleWindows: true, + only: 'chrome', + }; + + it('window.open', function () { + return runTests('./testcafe-fixtures/disable-multiple-windows.js', 'window.open', TEST_RUN_OPTIONS); + }); + + it('click on link', function () { + return runTests('./testcafe-fixtures/disable-multiple-windows.js', 'click on link', TEST_RUN_OPTIONS); + }); + + it('form submit', function () { + return runTests('./testcafe-fixtures/disable-multiple-windows.js', 'form submit', TEST_RUN_OPTIONS); + }); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7599/testcafe-fixtures/disable-multiple-windows.js b/test/functional/fixtures/regression/gh-7599/testcafe-fixtures/disable-multiple-windows.js new file mode 100644 index 00000000000..cdc2633fac9 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/testcafe-fixtures/disable-multiple-windows.js @@ -0,0 +1,27 @@ +import { Selector } from 'testcafe'; + +fixture `Fixture`; + +test + .page('http://localhost:3000/fixtures/regression/gh-7599/pages/disable-multiple-windows/window-open.html') + ('window.open', async t => { + await t + .click('#btn') + .expect(Selector('h1').textContent).eql('Child page'); + }); + +test + .page('http://localhost:3000/fixtures/regression/gh-7599/pages/disable-multiple-windows/click-on-link.html') + ('click on link', async t => { + await t + .click('#link') + .expect(Selector('h1').textContent).eql('Child page'); + }); + +test + .page('http://localhost:3000/fixtures/regression/gh-7599/pages/disable-multiple-windows/form-submit.html') + ('form submit', async t => { + await t + .click('#submitBtn') + .expect(Selector('h1').textContent).eql('Child page'); + }); diff --git a/test/functional/fixtures/regression/gh-7599/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7599/testcafe-fixtures/index.js new file mode 100644 index 00000000000..e52d4efd836 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7599/testcafe-fixtures/index.js @@ -0,0 +1,10 @@ +fixture `Multiple windows in the Native Automation mode` + .page `http://localhost:3000/fixtures/regression/gh-7599/pages/index.html`; + +test(`Should fail on link[target="blank"] click`, async t => { + await t.click('a'); +}); + +test(`Should fail on Multiple Window API call in the Native Automation mode`, async t => { + await t.openWindow('http://example.com'); +}); diff --git a/test/functional/fixtures/regression/gh-7632/pages/test1.html b/test/functional/fixtures/regression/gh-7632/pages/test1.html new file mode 100644 index 00000000000..1a535c1687a --- /dev/null +++ b/test/functional/fixtures/regression/gh-7632/pages/test1.html @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7632/pages/test2.html b/test/functional/fixtures/regression/gh-7632/pages/test2.html new file mode 100644 index 00000000000..694df18e657 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7632/pages/test2.html @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/test/functional/fixtures/regression/gh-7632/test.js b/test/functional/fixtures/regression/gh-7632/test.js new file mode 100644 index 00000000000..6725b8b421a --- /dev/null +++ b/test/functional/fixtures/regression/gh-7632/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-7632)', function () { + it('Esotope', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7632/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7632/testcafe-fixtures/index.js new file mode 100644 index 00000000000..1266e5762d2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7632/testcafe-fixtures/index.js @@ -0,0 +1,7 @@ +fixture `esotope`; + +test.page('http://localhost:3000/fixtures/regression/gh-7632/pages/test1.html')('PrivateIdentifier', () => { +}); + +test.page('http://localhost:3000/fixtures/regression/gh-7632/pages/test2.html')('PropertyDefinition', () => { +}); diff --git a/test/functional/fixtures/regression/gh-7634/pages/empty.zip b/test/functional/fixtures/regression/gh-7634/pages/empty.zip new file mode 100644 index 00000000000..2c9abba7e56 Binary files /dev/null and b/test/functional/fixtures/regression/gh-7634/pages/empty.zip differ diff --git a/test/functional/fixtures/regression/gh-7634/pages/index.html b/test/functional/fixtures/regression/gh-7634/pages/index.html new file mode 100644 index 00000000000..468b95f5470 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7634/pages/index.html @@ -0,0 +1,10 @@ + + + + + gh-7634 + + + Download a file + + diff --git a/test/functional/fixtures/regression/gh-7634/test.js b/test/functional/fixtures/regression/gh-7634/test.js new file mode 100644 index 00000000000..8c5cf7aaa26 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7634/test.js @@ -0,0 +1,23 @@ +const { join } = require('path'); +const { homedir } = require('os'); +const fs = require('fs'); +const { expect } = require('chai'); +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +const DOWNLOADED_FILE_PATH = join(homedir(), 'Downloads', 'empty.zip'); +const EXPECTED_FILE_SIZE = 144; + +describe('Download a file using the Native Automation mode', function () { + onlyInNativeAutomation('Download a file using the Native Automation mode', function () { + return runTests('testcafe-fixtures/index.js') + .then(() => { + const fileStat = fs.fstatSync(fs.openSync(DOWNLOADED_FILE_PATH)); + + expect(fileStat.size).eql(EXPECTED_FILE_SIZE); + + fs.rmSync(DOWNLOADED_FILE_PATH); + }); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7634/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7634/testcafe-fixtures/index.js new file mode 100644 index 00000000000..ae020eb43ad --- /dev/null +++ b/test/functional/fixtures/regression/gh-7634/testcafe-fixtures/index.js @@ -0,0 +1,10 @@ +import { Selector } from 'testcafe'; + +fixture `Download a file` + .page `http://localhost:3000/fixtures/regression/gh-7634/pages/index.html`; + + +test('Download a file', async t => { + await t.click(Selector('a')); +}); + diff --git a/test/functional/fixtures/regression/gh-7640-iframe/pages/dynamic-oopif.html b/test/functional/fixtures/regression/gh-7640-iframe/pages/dynamic-oopif.html new file mode 100644 index 00000000000..d761eab9f74 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7640-iframe/pages/dynamic-oopif.html @@ -0,0 +1,13 @@ + diff --git a/test/functional/fixtures/regression/gh-7640-iframe/pages/frame.html b/test/functional/fixtures/regression/gh-7640-iframe/pages/frame.html new file mode 100644 index 00000000000..16157c827c4 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7640-iframe/pages/frame.html @@ -0,0 +1,5 @@ + diff --git a/test/functional/fixtures/regression/gh-7640-iframe/test.js b/test/functional/fixtures/regression/gh-7640-iframe/test.js new file mode 100644 index 00000000000..bc0a7ea4365 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7640-iframe/test.js @@ -0,0 +1,53 @@ +const path = require('path'); +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); +const createTestCafe = require('../../../../../lib'); + +let testcafe = null; +let failedCount = 0; + +describe('[Regression](GH-7640-iframe)', function () { + onlyInNativeAutomation('Should handle requests in specific iframe in Native Automation mode (headless)', function () { + return createTestCafe('127.0.0.1', 1335, 1336) + .then(tc => { + testcafe = tc; + }) + .then(() => { + return testcafe.createRunner() + .browsers(`chrome:headless`) + .src(path.join(__dirname, './testcafe-fixtures/index.js')) + .run(); + }) + .then(failed => { + failedCount = failed; + + return testcafe.close(); + }) + .then(() => { + if (failedCount) + throw new Error('Error occurred'); + }); + }); + + onlyInNativeAutomation('Should handle requests in specific iframe in Native Automation mode (headed)', function () { + return createTestCafe('127.0.0.1', 1335, 1336) + .then(tc => { + testcafe = tc; + }) + .then(() => { + return testcafe.createRunner() + .browsers(`chrome`) + .src(path.join(__dirname, './testcafe-fixtures/index.js')) + .run(); + }) + .then(failed => { + failedCount = failed; + + return testcafe.close(); + }) + .then(() => { + if (failedCount) + throw new Error('Error occurred'); + }); + }); +}); + diff --git a/test/functional/fixtures/regression/gh-7640-iframe/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7640-iframe/testcafe-fixtures/index.js new file mode 100644 index 00000000000..23ef3603792 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7640-iframe/testcafe-fixtures/index.js @@ -0,0 +1,12 @@ +import { RequestLogger } from 'testcafe'; + +fixture `Should handle requests in specific iframe in Native Automation mode` + .page `http://localhost:3000/fixtures/regression/gh-7640-iframe/pages/dynamic-oopif.html`; + +const logger = new RequestLogger(); + +test.requestHooks(logger)(`Should handle requests in specific iframe in Native Automation mode`, async t => { + await t.switchToIframe('iframe'); + + await t.expect(logger.contains(r => r.request.url.includes('?test'))).ok(); +}); diff --git a/test/functional/fixtures/regression/gh-7640/pages/index.html b/test/functional/fixtures/regression/gh-7640/pages/index.html new file mode 100644 index 00000000000..18237aa1f1f --- /dev/null +++ b/test/functional/fixtures/regression/gh-7640/pages/index.html @@ -0,0 +1,22 @@ + + + + + gh-1999 + + + +
+ + + diff --git a/test/functional/fixtures/regression/gh-7640/test.js b/test/functional/fixtures/regression/gh-7640/test.js new file mode 100644 index 00000000000..f1b3732aa7b --- /dev/null +++ b/test/functional/fixtures/regression/gh-7640/test.js @@ -0,0 +1,9 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('Redirect on the Request Hook', function () { + onlyInNativeAutomation('Redirect on the Request Hook', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7640/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7640/testcafe-fixtures/index.js new file mode 100644 index 00000000000..420fa68fa20 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7640/testcafe-fixtures/index.js @@ -0,0 +1,33 @@ +import { RequestHook, Selector } from 'testcafe'; + +class MyRequestHook extends RequestHook { + constructor (requestFilterRules, responseEventConfigureOpts) { + super(requestFilterRules, responseEventConfigureOpts); + } + + async onRequest (e) { + e.requestOptions.hostname = 'localhost'; + e.requestOptions.port = 3001; + } + + async onResponse () { + } +} + +const customHook = new MyRequestHook(/api/); +const logger = Selector('div'); + +fixture `Redirect on the Request Hook` + .page `http://localhost:3000/fixtures/regression/gh-7640/pages/index.html`; + +test('No redirect', async t => { + await t.click('button'); + + await t.expect(logger.innerText).eql('{"name":"John Hearts","position":"CTO"}'); +}); + +test.requestHooks(customHook)(`Redirect on the Request Hook`, async t => { + await t.click('button'); + + await t.expect(logger.innerText).eql('{"name":"James Livers","position":"CEO"}'); +}); diff --git a/test/functional/fixtures/regression/gh-7667/pages/index.html b/test/functional/fixtures/regression/gh-7667/pages/index.html new file mode 100644 index 00000000000..66889efa549 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7667/pages/index.html @@ -0,0 +1,9 @@ + + + + gh-7667 + + + + + diff --git a/test/functional/fixtures/regression/gh-7667/test.js b/test/functional/fixtures/regression/gh-7667/test.js new file mode 100644 index 00000000000..eed17ce203a --- /dev/null +++ b/test/functional/fixtures/regression/gh-7667/test.js @@ -0,0 +1,7 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('[Regression](GH-7667)', function () { + onlyInNativeAutomation('Should select and remove text from input', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7667/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7667/testcafe-fixtures/index.js new file mode 100644 index 00000000000..f13d7fafe79 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7667/testcafe-fixtures/index.js @@ -0,0 +1,22 @@ +import { Selector } from 'testcafe'; + +fixture `GH-7667 - 'ctrl+a' key combination doesn't work in NA mode` + .page `http://localhost:3000/fixtures/regression/gh-7667/pages/index.html`; + +test(`Delete whole text from input via 'ctrl+a delete'`, async t => { + const input = Selector('#input'); + + await t + .click(input) + .pressKey('ctrl+a delete') + .expect(input.value).eql(''); +}); + +test(`Delete whole text from input via 'Ctrl+A delete'`, async t => { + const input = Selector('#input'); + + await t + .click(input) + .pressKey('Ctrl+A delete') + .expect(input.value).eql(''); +}); diff --git a/test/functional/fixtures/regression/gh-7675/pages/foo.js b/test/functional/fixtures/regression/gh-7675/pages/foo.js new file mode 100644 index 00000000000..c6035e5e3f3 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7675/pages/foo.js @@ -0,0 +1 @@ +// do nothing diff --git a/test/functional/fixtures/regression/gh-7675/pages/import.html b/test/functional/fixtures/regression/gh-7675/pages/import.html new file mode 100644 index 00000000000..a77b267b39d --- /dev/null +++ b/test/functional/fixtures/regression/gh-7675/pages/import.html @@ -0,0 +1,13 @@ + + + + + Worker + + +

Worker

+ + + diff --git a/test/functional/fixtures/regression/gh-7675/pages/worker1.js b/test/functional/fixtures/regression/gh-7675/pages/worker1.js new file mode 100644 index 00000000000..fff9d0b3334 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7675/pages/worker1.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-undef +importScripts('http://localhost:3000/fixtures/regression/gh-7675/pages/foo.js'); diff --git a/test/functional/fixtures/regression/gh-7675/pages/worker2.js b/test/functional/fixtures/regression/gh-7675/pages/worker2.js new file mode 100644 index 00000000000..c8f0f18d394 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7675/pages/worker2.js @@ -0,0 +1,11 @@ +(function sendXHR () { + return new Promise(() => { + fetch('http://localhost:3000/fixtures/regression/gh-7675/pages/foo.js') + .then(res => res.text()) + .then(text => { + self.postMessage({ + foo: text, + }); + }); + }); +})(); diff --git a/test/functional/fixtures/regression/gh-7675/pages/xhr.html b/test/functional/fixtures/regression/gh-7675/pages/xhr.html new file mode 100644 index 00000000000..212c125d3e4 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7675/pages/xhr.html @@ -0,0 +1,19 @@ + + + + + Worker + + +

Worker

+ + + diff --git a/test/functional/fixtures/regression/gh-7675/test.js b/test/functional/fixtures/regression/gh-7675/test.js new file mode 100644 index 00000000000..498c1b77f63 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7675/test.js @@ -0,0 +1,11 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('Requests from worker should not fail in native automation', function () { + onlyInNativeAutomation('`importScripts` in worker should not fail in native automation', function () { + return runTests('testcafe-fixtures/index.js', '`importScripts` in worker should not fail in native automation'); + }); + + onlyInNativeAutomation('The `XHR` request in worker should not fail in native automation', function () { + return runTests('testcafe-fixtures/index.js', 'The `XHR` request in worker should not fail in native automation'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7675/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7675/testcafe-fixtures/index.js new file mode 100644 index 00000000000..fce3dcc107b --- /dev/null +++ b/test/functional/fixtures/regression/gh-7675/testcafe-fixtures/index.js @@ -0,0 +1,13 @@ +import { Selector } from 'testcafe'; + +fixture `Requests from worker should not fail in native automation`; + +test.page('http://localhost:3000/fixtures/regression/gh-7675/pages/import.html') +('`importScripts` in worker should not fail in native automation', async () => { +}); + +test.page('http://localhost:3000/fixtures/regression/gh-7675/pages/xhr.html') +('The `XHR` request in worker should not fail in native automation', async t => { + await t.expect(Selector('h1').innerText).eql('// do nothing'); +}); + diff --git a/test/functional/fixtures/regression/gh-770/pages/index.html b/test/functional/fixtures/regression/gh-770/pages/index.html index e7a38e1cfd2..db9271bc98a 100644 --- a/test/functional/fixtures/regression/gh-770/pages/index.html +++ b/test/functional/fixtures/regression/gh-770/pages/index.html @@ -7,16 +7,12 @@
-
- \ No newline at end of file + diff --git a/test/functional/fixtures/regression/gh-770/test.js b/test/functional/fixtures/regression/gh-770/test.js index a156d15d3c5..de3a22b8b7c 100644 --- a/test/functional/fixtures/regression/gh-770/test.js +++ b/test/functional/fixtures/regression/gh-770/test.js @@ -2,8 +2,4 @@ describe('[Regression](GH-770)', function () { it("Shouldn't focus on non-focusable element while clicking", function () { return runTests('./testcafe-fixtures/index.test.js', 'click non-focusable div'); }); - - it('The element should remain active in IE if it has been focused while mousedown executed', function () { - return runTests('testcafe-fixtures/index.test.js', 'Click on div', { only: ['ie', 'ie 9', 'ie 10', 'edge'] }); - }); }); diff --git a/test/functional/fixtures/regression/gh-770/testcafe-fixtures/index.test.js b/test/functional/fixtures/regression/gh-770/testcafe-fixtures/index.test.js index 35d81462775..743eabd80a6 100644 --- a/test/functional/fixtures/regression/gh-770/testcafe-fixtures/index.test.js +++ b/test/functional/fixtures/regression/gh-770/testcafe-fixtures/index.test.js @@ -5,7 +5,6 @@ import { expect } from 'chai'; fixture `GH-770` .page `http://localhost:3000/fixtures/regression/gh-770/pages/index.html`; -const isInputActiveElement = ClientFunction(() => document.activeElement === document.getElementById('input')); const isDocumentBodyActiveElement = ClientFunction(() => document.activeElement === document.body); @@ -14,9 +13,3 @@ test('click non-focusable div', async t => { expect(await isDocumentBodyActiveElement()).to.be.true; }); - -test('Click on div', async t => { - await t.click('#forceInputFocusDiv'); - - expect(await isInputActiveElement()).to.be.true; -}); diff --git a/test/functional/fixtures/regression/gh-7748/pages/index.html b/test/functional/fixtures/regression/gh-7748/pages/index.html new file mode 100644 index 00000000000..d6c690ff743 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7748/pages/index.html @@ -0,0 +1,15 @@ + + + + + + +
+
+ + + diff --git a/test/functional/fixtures/regression/gh-7748/test.js b/test/functional/fixtures/regression/gh-7748/test.js new file mode 100644 index 00000000000..64eae1f3c05 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7748/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-7747)', function () { + it('Should modify header on RequestHook', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7748/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7748/testcafe-fixtures/index.js new file mode 100644 index 00000000000..a5dde8031b1 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7748/testcafe-fixtures/index.js @@ -0,0 +1,22 @@ +import { RequestHook, Selector } from 'testcafe'; + +export default class CustomRequestHook extends RequestHook { + constructor (requestFilterRules, responseEventConfigureOpts) { + super(requestFilterRules, responseEventConfigureOpts); + } + + async onRequest (requestEvent) { + requestEvent.requestOptions.headers['user-agent'] = 'test'; + } + + async onResponse () { + } +} + +fixture `Should modify header on RequestHook` + .page('http://localhost:3000/fixtures/regression/gh-7748/pages/index.html') + .requestHooks(new CustomRequestHook()); + +test('Set / override request headers', async t => { + await t.expect(Selector('#target').innerText).eql('Other'); +}); diff --git a/test/functional/fixtures/regression/gh-7764/pages/index.html b/test/functional/fixtures/regression/gh-7764/pages/index.html new file mode 100644 index 00000000000..4f0ee91e533 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7764/pages/index.html @@ -0,0 +1,9 @@ + + + + + gh-7764 + + + + diff --git a/test/functional/fixtures/regression/gh-7764/test.js b/test/functional/fixtures/regression/gh-7764/test.js new file mode 100644 index 00000000000..0507ea4abeb --- /dev/null +++ b/test/functional/fixtures/regression/gh-7764/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-7764)', function () { + it('Request logger should contain actual headers if RequestHook modified them', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7764/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7764/testcafe-fixtures/index.js new file mode 100644 index 00000000000..a69e20a2eb4 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7764/testcafe-fixtures/index.js @@ -0,0 +1,29 @@ +import { RequestLogger, RequestHook } from 'testcafe'; + +fixture`Set a Custom Referer` + .page`http://localhost:3000/fixtures/regression/gh-7764/pages/index.html`; + +export class MyRequestHook extends RequestHook { + constructor (requestFilterRules, responseEventConfigureOpts) { + super(requestFilterRules, responseEventConfigureOpts); + } + + async onRequest (event) { + event.requestOptions.headers['referer'] = 'http://my-modified-referer.com'; + } + + async onResponse () { + } +} + +const hook = new MyRequestHook(); + +const logger = RequestLogger('http://localhost:3000/fixtures/regression/gh-7764/pages/index.html', { + logRequestHeaders: true, +}); + +test + .requestHooks([hook, logger]) + ('Request logger should contain actual headers if RequestHook modified them', async t => { + await t.expect(logger.requests[0].request.headers['referer']).eql('http://my-modified-referer.com'); + }); diff --git a/test/functional/fixtures/regression/gh-7770/pages/frame.html b/test/functional/fixtures/regression/gh-7770/pages/frame.html new file mode 100644 index 00000000000..83cfc80677b --- /dev/null +++ b/test/functional/fixtures/regression/gh-7770/pages/frame.html @@ -0,0 +1,21 @@ + + + + + Worker + + +

Worker Frame

+ + + diff --git a/test/functional/fixtures/regression/gh-7770/pages/index.html b/test/functional/fixtures/regression/gh-7770/pages/index.html new file mode 100644 index 00000000000..04056e45406 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7770/pages/index.html @@ -0,0 +1,10 @@ + + + + + gh-7770 + + + + + diff --git a/test/functional/fixtures/regression/gh-7770/pages/worker.js b/test/functional/fixtures/regression/gh-7770/pages/worker.js new file mode 100644 index 00000000000..70d6b26ac4e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7770/pages/worker.js @@ -0,0 +1,5 @@ +setTimeout(function () { + self.postMessage('Header is set from worker'); + + fetch('http://localhost:3000/?fromWorker'); +}, 1000); diff --git a/test/functional/fixtures/regression/gh-7770/test.js b/test/functional/fixtures/regression/gh-7770/test.js new file mode 100644 index 00000000000..6e76c44efae --- /dev/null +++ b/test/functional/fixtures/regression/gh-7770/test.js @@ -0,0 +1,53 @@ +const path = require('path'); +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); +const createTestCafe = require('../../../../../lib'); + +let testcafe = null; +let failedCount = 0; + +describe('[Regression](GH-7770)', function () { + onlyInNativeAutomation('Should handle iframe + worker in Native Automation mode (headless)', function () { + return createTestCafe('127.0.0.1', 1335, 1336) + .then(tc => { + testcafe = tc; + }) + .then(() => { + return testcafe.createRunner() + .browsers(`chrome:headless`) + .src(path.join(__dirname, './testcafe-fixtures/index.js')) + .run(); + }) + .then(failed => { + failedCount = failed; + + return testcafe.close(); + }) + .then(() => { + if (failedCount) + throw new Error('Error occurred'); + }); + }); + + onlyInNativeAutomation('Should handle iframe + worker in Native Automation mode (headed)', function () { + return createTestCafe('127.0.0.1', 1335, 1336) + .then(tc => { + testcafe = tc; + }) + .then(() => { + return testcafe.createRunner() + .browsers(`chrome`) + .src(path.join(__dirname, './testcafe-fixtures/index.js')) + .run(); + }) + .then(failed => { + failedCount = failed; + + return testcafe.close(); + }) + .then(() => { + if (failedCount) + throw new Error('Error occurred'); + }); + }); +}); + diff --git a/test/functional/fixtures/regression/gh-7770/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7770/testcafe-fixtures/index.js new file mode 100644 index 00000000000..ad2e42a0714 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7770/testcafe-fixtures/index.js @@ -0,0 +1,16 @@ +import { Selector, RequestLogger } from 'testcafe'; + +const logger = new RequestLogger(); + +fixture `Should handle iframe + worker in Native Automation mode` + .page `http://localhost:3000/fixtures/regression/gh-7770/pages/index.html` + .requestHooks(logger); + +test('Should handle iframe + worker in Native Automation mode', async t => { + await t.switchToIframe('iframe'); + + await t.expect(Selector('h1').innerText).eql('Header is set from worker'); + + await t.expect(logger.contains(r => r.request.url === 'http://localhost:3000/?fromIFrame')).ok(); + await t.expect(logger.contains(r => r.request.url === 'http://localhost:3000/?fromWorker')).ok(); +}); diff --git a/test/functional/fixtures/regression/gh-7783/test.js b/test/functional/fixtures/regression/gh-7783/test.js new file mode 100644 index 00000000000..40c47143887 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7783/test.js @@ -0,0 +1,7 @@ +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('[Regression](GH-7783)', function () { + onlyInNativeAutomation('Trim BOM symbol in Native Automation', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); diff --git a/test/functional/fixtures/regression/gh-7783/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7783/testcafe-fixtures/index.js new file mode 100644 index 00000000000..0a202e57790 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7783/testcafe-fixtures/index.js @@ -0,0 +1,8 @@ +import { Selector } from 'testcafe'; + +fixture `Trim BOM symbol in Native Automation` + .page `http://localhost:3000/trim-bom`; + +test('Trim BOM symbol in Native Automation', async t => { + await t.expect(Selector(() => document.body.children[0]).tagName).eql('button'); +}); diff --git a/test/functional/fixtures/regression/gh-7787/pages/index.html b/test/functional/fixtures/regression/gh-7787/pages/index.html new file mode 100644 index 00000000000..53228a7d0b1 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7787/pages/index.html @@ -0,0 +1,12 @@ + + + + + Title + + + + + diff --git a/test/functional/fixtures/regression/gh-7787/test.js b/test/functional/fixtures/regression/gh-7787/test.js new file mode 100644 index 00000000000..d7db1686e5a --- /dev/null +++ b/test/functional/fixtures/regression/gh-7787/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-7787)', function () { + it('Should not fail if `statusCode` is set as string', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7787/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7787/testcafe-fixtures/index.js new file mode 100644 index 00000000000..6576222b26b --- /dev/null +++ b/test/functional/fixtures/regression/gh-7787/testcafe-fixtures/index.js @@ -0,0 +1,18 @@ +import { RequestMock } from 'testcafe'; + +let logged = false; + +const mock = RequestMock() + .onRequestTo(/custom/) + .respond((req, res) => { + res.statusCode = '200'; + logged = true; + }); + +fixture `Should not fail if \`statusCode\` is set as string` + .page`http://localhost:3000/fixtures/regression/gh-7787/pages/index.html` + .requestHooks(mock); + +test('Should not fail if `statusCode` is set as string', async t => { + await t.expect(logged).ok(); +}); diff --git a/test/functional/fixtures/regression/gh-7793/pages/index.html b/test/functional/fixtures/regression/gh-7793/pages/index.html new file mode 100644 index 00000000000..aeb08fc18fc --- /dev/null +++ b/test/functional/fixtures/regression/gh-7793/pages/index.html @@ -0,0 +1,9 @@ + + + + + gh-7793 + + + + diff --git a/test/functional/fixtures/regression/gh-7793/test.js b/test/functional/fixtures/regression/gh-7793/test.js new file mode 100644 index 00000000000..51f1a6c2bc2 --- /dev/null +++ b/test/functional/fixtures/regression/gh-7793/test.js @@ -0,0 +1,7 @@ +describe('[Regression](GH-7793)', function () { + it('Should set cookies with the `httpOnly` option', function () { + return runTests('testcafe-fixtures/index.js'); + }); +}); + + diff --git a/test/functional/fixtures/regression/gh-7793/testcafe-fixtures/index.js b/test/functional/fixtures/regression/gh-7793/testcafe-fixtures/index.js new file mode 100644 index 00000000000..a45610f8c0d --- /dev/null +++ b/test/functional/fixtures/regression/gh-7793/testcafe-fixtures/index.js @@ -0,0 +1,14 @@ +fixture `Should set cookie with the \`httpOnly\` option` + .page `http://localhost:3000/fixtures/regression/gh-7793/pages/index.html`; + +test('Should set cookies with the `httpOnly` option', async t => { + const cookieObject = { name: 'apiCookie1', value: 'value1', domain: 'localhost', httpOnly: true }; + + await t.setCookies(cookieObject); + + const cookies = await t.getCookies(); + + await t + .expect(cookies.length).eql(1) + .expect(cookies[0]).contains(cookieObject); +}); diff --git a/test/functional/fixtures/regression/gh-7797/pages/frame.html b/test/functional/fixtures/regression/gh-7797/pages/frame.html new file mode 100644 index 00000000000..0822a0fc22a --- /dev/null +++ b/test/functional/fixtures/regression/gh-7797/pages/frame.html @@ -0,0 +1,23 @@ + + + + + gh-7797 + + + +
+ + + + diff --git a/test/functional/fixtures/regression/gh-7797/pages/index.html b/test/functional/fixtures/regression/gh-7797/pages/index.html new file mode 100644 index 00000000000..3a12d23792e --- /dev/null +++ b/test/functional/fixtures/regression/gh-7797/pages/index.html @@ -0,0 +1,17 @@ + + + + + gh-7797 + + + + + + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-before-and-after.html b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-before-and-after.html new file mode 100644 index 00000000000..8d1d1bcfd34 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-before-and-after.html @@ -0,0 +1,18 @@ + + + + Test page title + + + + + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-only-in-body.html b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-only-in-body.html new file mode 100644 index 00000000000..7abbe928973 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-only-in-body.html @@ -0,0 +1,14 @@ + + + + + Test page title + + + + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/title-is-not-last.html b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/title-is-not-last.html new file mode 100644 index 00000000000..81aaec1cb59 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/initial-value/title-is-not-last.html @@ -0,0 +1,15 @@ + + + + + Test page title + + + + + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/pages/set-document-title-in-body.html b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/set-document-title-in-body.html new file mode 100644 index 00000000000..63986e4bcc2 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/set-document-title-in-body.html @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/pages/text-property-getters-of-title-element.html b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/text-property-getters-of-title-element.html new file mode 100644 index 00000000000..e12e48a781d --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/pages/text-property-getters-of-title-element.html @@ -0,0 +1,16 @@ + + + + Test title + + + + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/test.js b/test/functional/fixtures/regression/hammerhead/gh-2350/test.js new file mode 100644 index 00000000000..a55c42018fd --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/test.js @@ -0,0 +1,41 @@ +const { skipInNativeAutomation } = require('../../../../utils/skip-in'); + +describe("Should provide a valid value for the 'document.title' property", () => { + describe('Initial value', () => { + // NOTE: in the proxy mode we have some additional scripts for processing document.title + // we do not have these scripts in the nativeAutomation mode yet + it('script before and after ', () => { + return runTests('./testcafe-fixtures/index.js', 'script before and after <title>'); + }); + + it('script tag only in <body>', () => { + return runTests('./testcafe-fixtures/index.js', 'script tag only in <body>'); + }); + + it('script tag only in <body> and <title> is not last in <head>', () => { + return runTests('./testcafe-fixtures/index.js', 'script tag only in <body> and <title> is not last in <head>'); + }); + }); + + it('Empty value', () => { + return runTests('./testcafe-fixtures/index.js', 'empty value'); + }); + + // NOTE: in the proxy mode we have some additional scripts for processing document.title + // we do not have these scripts in the nativeAutomation mode yet + skipInNativeAutomation('Change value', () => { + return runTests('./testcafe-fixtures/index.js', 'change value'); + }); + + it('Text property getters of the title element', () => { + return runTests('./testcafe-fixtures/index.js', 'text property getters of the title element'); + }); + + it('Set document.title in body', () => { + return runTests('./testcafe-fixtures/index.js', 'set document.title in body'); + }); + + it('Iframes', () => { + return runTests('./testcafe-fixtures/iframes.js'); + }); +}); diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/testcafe-fixtures/iframes.js b/test/functional/fixtures/regression/hammerhead/gh-2350/testcafe-fixtures/iframes.js new file mode 100644 index 00000000000..850595b2646 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/testcafe-fixtures/iframes.js @@ -0,0 +1,27 @@ +import { ClientFunction } from 'testcafe'; + +fixture `Fixture` + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/iframes/index.html'); + +const getNativeTitle = ClientFunction(() => { + var hammerhead = window['%hammerhead%']; // eslint-disable-line no-var + + return hammerhead.nativeMethods.documentTitleGetter.call(document); +}); + +test('test', async t => { + const nativeAutomation = t.testRun.opts.nativeAutomation; + const nativeTitle = await getNativeTitle(); + + if (nativeAutomation) + await t.expect(nativeTitle).eql('Index page title'); + else + await t.expect(nativeTitle).notEql('Index page title'); + + await t + .switchToIframe('#withSrc') + .expect(getNativeTitle()).eql('Iframe page title') + .switchToMainWindow() + .switchToIframe('#withoutSrc') + .expect(getNativeTitle()).eql(''); +}); diff --git a/test/functional/fixtures/regression/hammerhead/gh-2350/testcafe-fixtures/index.js b/test/functional/fixtures/regression/hammerhead/gh-2350/testcafe-fixtures/index.js new file mode 100644 index 00000000000..85313ee8506 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-2350/testcafe-fixtures/index.js @@ -0,0 +1,83 @@ +import { ClientFunction } from 'testcafe'; + +fixture `Fixture`; + +const getLog = ClientFunction(() => window.log); + +const INITIAL_VALUE = { + SCRIPT_BEFORE_AND_AFTER_TITLE_EXPECTED_LOG: [ + 'head:before-first-title: ', + 'head:after-first-title: Test page title', + 'body: Test page title', + ], + SCRIPT_ONLY_IN_BODY_EXPECTED_LOG: [ + 'body: Test page title', + ], + SCRIPT_ONLY_IN_BODY_AND_TITLE_IS_NOT_LAST_EXPECTED_LOG: [ + 'body: Test page title', + ], +}; + +const EMPTY_VALUE_EXPECTED_LOG = [ + 'head: ', + 'body: ', +]; + +const CHANGE_VALUE_EXPECTED_LOG = [ + 'head:after-first-title: Test page title 1', + 'head:after-title-update: Test page title 2', + 'body: Test page title 2', + 'body:after-title-update: Test page title 3', +]; + +const TEXT_PROPERTY_GETTERS_OF_TITLE_ELEMENT_EXPECTED_LOG = [ + 'text: Test title', + 'innerHTML: Test title', + 'innerText: Test title', +]; + +const SET_DOCUMENT_TITLE_IN_BODY_EXPECTED_LOG = [ + 'body: Test page title', +]; + +test + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-before-and-after.html') + ('script before and after <title>', async t => { + await t.expect(getLog()).eql(INITIAL_VALUE.SCRIPT_BEFORE_AND_AFTER_TITLE_EXPECTED_LOG); + }); + +test + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/initial-value/script-only-in-body.html') + ('script tag only in <body>', async t => { + await t.expect(getLog()).eql(INITIAL_VALUE.SCRIPT_ONLY_IN_BODY_EXPECTED_LOG); + }); + +test + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/initial-value/title-is-not-last.html') + ('script tag only in <body> and <title> is not last in <head>', async t => { + await t.expect(getLog()).eql(INITIAL_VALUE.SCRIPT_ONLY_IN_BODY_AND_TITLE_IS_NOT_LAST_EXPECTED_LOG); + }); + +test + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/empty-value.html') + ('empty value', async t => { + await t.expect(getLog()).eql(EMPTY_VALUE_EXPECTED_LOG); + }); + +test + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/change-value.html') + ('change value', async t => { + await t.expect(getLog()).eql(CHANGE_VALUE_EXPECTED_LOG); + }); + +test + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/text-property-getters-of-title-element.html') + ('text property getters of the title element', async t => { + await t.expect(getLog()).eql(TEXT_PROPERTY_GETTERS_OF_TITLE_ELEMENT_EXPECTED_LOG); + }); + +test + .page('http://localhost:3000/fixtures/regression/hammerhead/gh-2350/pages/set-document-title-in-body.html') + ('set document.title in body', async t => { + await t.expect(getLog()).eql(SET_DOCUMENT_TITLE_IN_BODY_EXPECTED_LOG); + }); diff --git a/test/functional/fixtures/regression/hammerhead/gh-863/page/img.png b/test/functional/fixtures/regression/hammerhead/gh-863/page/img.png new file mode 100644 index 00000000000..62eae71c3f7 Binary files /dev/null and b/test/functional/fixtures/regression/hammerhead/gh-863/page/img.png differ diff --git a/test/functional/fixtures/regression/hammerhead/gh-863/page/index.html b/test/functional/fixtures/regression/hammerhead/gh-863/page/index.html new file mode 100644 index 00000000000..2d65928b098 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-863/page/index.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Test page + + + +
+
+
+ + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-863/page/script-1.js b/test/functional/fixtures/regression/hammerhead/gh-863/page/script-1.js new file mode 100644 index 00000000000..7f74f4725a5 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-863/page/script-1.js @@ -0,0 +1,8 @@ +document.addEventListener('DOMContentLoaded', () => { + const div = document.createElement('div'); + + div.textContent = 'Test element'; + div.id = 'test-div'; + + document.body.appendChild(div); +}); diff --git a/test/functional/fixtures/regression/hammerhead/gh-863/page/script-2.js b/test/functional/fixtures/regression/hammerhead/gh-863/page/script-2.js new file mode 100644 index 00000000000..9e160885d42 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-863/page/script-2.js @@ -0,0 +1,3 @@ +window['testVar'] = 123; + + diff --git a/test/functional/fixtures/regression/hammerhead/gh-863/test.js b/test/functional/fixtures/regression/hammerhead/gh-863/test.js new file mode 100644 index 00000000000..18038c55de3 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-863/test.js @@ -0,0 +1,104 @@ +const http = require('http'); +const path = require('path'); +const fs = require('fs'); +const createTestCafe = require('../../../../../../lib'); +const config = require('../../../../config'); +const { expect } = require('chai'); +const { getFreePort } = require('../../../../../../lib/utils/endpoint-utils'); + +const resourceRequestCounter = { + script1: 0, + script2: 0, + img: 0, +}; + +function resolvePath (file) { + return path.join(__dirname, file); +} + +function readFileContent (file) { + return fs.readFileSync(resolvePath(file)).toString(); +} + +function addCachingHeader (res) { + res.setHeader('cache-control', 'max-age=86400'); // Cache response 1 day +} + +async function createServer () { + const server = http.createServer((req, res) => { + let content = ''; + + if (req.url === '/') + content = readFileContent('./page/index.html'); + + else if (req.url === '/script-1.js') { + resourceRequestCounter.script1++; + + addCachingHeader(res); + + content = readFileContent('./page/script-1.js'); + } + else if (req.url === '/script-2.js') { + resourceRequestCounter.script2++; + + addCachingHeader(res); + res.setHeader('X-Checked-Header', 'TesT'); + res.setHeader('content-type', 'application/javascript; charset=utf-8'); + + content = readFileContent('./page/script-2.js'); + } + else if (req.url === '/img.png') { + resourceRequestCounter.img++; + + addCachingHeader(res); + res.setHeader('content-type', 'image/png'); + fs.createReadStream(resolvePath('./page/img.png')).pipe(res); + + return; + } + + res.end(content); + }); + const port = await getFreePort(); + + process.env.TEST_SERVER_PORT = port.toString(); + + server.listen(port); + + return server; +} + +async function run ({ src, browser }) { + const testcafe = await createTestCafe({ + hostname: 'localhost', + port1: 1335, + port2: 1336, + cache: true, + }); + + await testcafe.createRunner() + .src(path.join(__dirname, src)) + .browsers(browser) + .run({ disableNativeAutomation: !config.nativeAutomation }); + + await testcafe.close(); +} + +const isLocalChrome = config.useLocalBrowsers && config.browsers.some(browser => browser.alias.includes('chrome')); + +if (isLocalChrome) { + describe('Cache', () => { + it('Should cache resources between tests', async () => { + const server = await createServer(); + + return run({ src: './testcafe-fixtures/index.js', browser: 'chrome:headless' }) + .then(() => { + server.close(); + + expect(resourceRequestCounter.script1).eql(1); + expect(resourceRequestCounter.script2).eql(1); + expect(resourceRequestCounter.img).eql(1); + }); + }); + }); +} diff --git a/test/functional/fixtures/regression/hammerhead/gh-863/testcafe-fixtures/index.js b/test/functional/fixtures/regression/hammerhead/gh-863/testcafe-fixtures/index.js new file mode 100644 index 00000000000..46bd7ca1578 --- /dev/null +++ b/test/functional/fixtures/regression/hammerhead/gh-863/testcafe-fixtures/index.js @@ -0,0 +1,34 @@ +import { Selector, t, ClientFunction } from 'testcafe'; + +const reload = ClientFunction(() => { + location.reload(true); +}); + +fixture `Fixture` + .page(`http://localhost:${process.env.TEST_SERVER_PORT}/`); + +async function assertTestElements () { + await t + .expect(Selector('#test-div').visible).ok() + .expect(Selector('#loaded-script-status').textContent).eql('Loaded') + .expect(Selector('#check-loaded-script-header-status').textContent).eql('Success') + .expect(Selector('#loaded-image-status').textContent).eql('Loaded'); +} + +async function performTestActions () { + await assertTestElements(); + await reload(); + await assertTestElements(); +} + +test('1', async () => { + await performTestActions(); +}); + +test('2', async () => { + await performTestActions(); +}); + +test('3', async () => { + await performTestActions(); +}); diff --git a/test/functional/fixtures/reporter/configs/xunit-config.js b/test/functional/fixtures/reporter/configs/xunit-config.js new file mode 100644 index 00000000000..616517f13e9 --- /dev/null +++ b/test/functional/fixtures/reporter/configs/xunit-config.js @@ -0,0 +1,23 @@ +const config = require('../../../config'); + +module.exports = { + hostname: config.testCafe.hostname, + port1: 1335, + port2: 1336, + developmentMode: config.devMode, + retryTestPages: config.retryTestPages, + nativeAutomation: config.nativeAutomation, + src: './test/functional/fixtures/reporter/testcafe-fixtures/index-test.js', + selectorTimeout: 200, + assertionTimeout: 1000, + pageLoadTimeout: 0, + reporter: { + name: 'xunit', + output: './test/functional/fixtures/reporter/report.xml', + }, + filter: { + test: 'Simple test', + }, + ...config.currentEnvironment, + browsers: config.browsers.map(browser => browser.browserName), +}; diff --git a/test/functional/fixtures/reporter/pages/index.html b/test/functional/fixtures/reporter/pages/index.html new file mode 100644 index 00000000000..b5930e31041 --- /dev/null +++ b/test/functional/fixtures/reporter/pages/index.html @@ -0,0 +1,24 @@ + + + + + Reporter + + + +
target
+ + +
+ + + + + + diff --git a/test/functional/fixtures/reporter/pages/snapshots.html b/test/functional/fixtures/reporter/pages/snapshots.html new file mode 100644 index 00000000000..63470300be9 --- /dev/null +++ b/test/functional/fixtures/reporter/pages/snapshots.html @@ -0,0 +1,47 @@ + + + + + Reporter + + + +
+
+ +
+ +

p1

p2

+ + + diff --git a/test/functional/fixtures/reporter/reporter.js b/test/functional/fixtures/reporter/reporter.js new file mode 100644 index 00000000000..9fb379fe016 --- /dev/null +++ b/test/functional/fixtures/reporter/reporter.js @@ -0,0 +1,101 @@ +const baseReport = { + async reportTaskStart () { + }, + async reportFixtureStart () { + }, + async reportTestDone () { + }, + async reportTaskDone () { + }, +}; + +function deleteCommandActionId (command) { + if (!command.actionId) + throw new Error('command does not have action id'); + + delete command.actionId; +} + +function generateReporter (log, options = {}) { + const { + emitOnStart = true, + emitOnDone = true, + includeBrowserInfo = false, + includeTestInfo = false, + includeCommandInfo = false, + } = options; + + return function () { + return Object.assign({}, baseReport, { + async reportTestActionStart (name, { browser, test, fixture, command }) { + if (!emitOnStart) + return; + + deleteCommandActionId(command); + + const item = { action: 'start', name }; + + if (includeBrowserInfo) + item.browser = browser.alias.split(':')[0]; + + if (includeTestInfo) { + if (test.id) { + item.test = { + id: 'test-id', + name: test.name, + phase: test.phase, + }; + } + + if (fixture.id) { + item.fixture = { + id: 'fixture-id', + name: fixture.name, + }; + } + } + + if (includeCommandInfo) + item.command = command; + + log.push(item); + }, + + async reportTestActionDone (name, { command, test, fixture, err }) { + if (!emitOnDone) + return; + + deleteCommandActionId(command); + + if (command.selector) + command.selector = command.selector.expression; + + const item = { name, action: 'done', command }; + + if (err) + item.err = err.code; + + if (includeTestInfo) { + if (test.id) { + item.test = { + id: 'test-id', + name: test.name, + phase: test.phase, + }; + } + + if (fixture.id) { + item.fixture = { + id: 'fixture-id', + name: fixture.name, + }; + } + } + + log.push(item); + }, + }); + }; +} + +module.exports = generateReporter; diff --git a/test/functional/fixtures/reporter/test.js b/test/functional/fixtures/reporter/test.js new file mode 100644 index 00000000000..74c42c87bc9 --- /dev/null +++ b/test/functional/fixtures/reporter/test.js @@ -0,0 +1,1410 @@ +const { expect } = require('chai'); +const fs = require('fs'); +const path = require('path'); +const { noop } = require('lodash'); +const generateReporter = require('./reporter'); +const { createReporter } = require('../../utils/reporter'); +const { createWarningReporter } = require('../../utils/warning-reporter'); +const ReporterPluginMethod = require('../../../../lib/reporter/plugin-methods'); +const assertionHelper = require('../../assertion-helper.js'); +const config = require('../../config'); +const { skipInNativeAutomation } = require('../../utils/skip-in'); +const getTestCafeVersion = require('../../../../lib/utils/get-testcafe-version'); + +const { + createSimpleTestStream, + createAsyncTestStream, + createSyncTestStream, +} = require('../../utils/stream'); +const runTestsWithConfig = require('../../utils/run-tests-with-config'); +const del = require('del'); + + +(config.hasBrowser('chrome') ? describe : describe.skip)('Reporter', () => { + const stdoutWrite = process.stdout.write; + const stderrWrite = process.stderr.write; + + afterEach(() => { + process.stdout.write = stdoutWrite; + process.stderr.write = stdoutWrite; + }); + + it('Should support several different reporters for a test run', function () { + const stream1 = createSimpleTestStream(); + const stream2 = createSimpleTestStream(); + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', { + only: ['chrome'], + reporter: [ + { + name: 'json', + output: stream1, + }, + { + name: 'list', + output: stream2, + }, + ], + }) + .then(() => { + expect(stream1.data).to.contains('Chrome'); + expect(stream1.data).to.contains('Reporter'); + expect(stream1.data).to.contains('Simple test'); + expect(stream2.data).to.contains('Chrome'); + expect(stream2.data).to.contains('Reporter'); + expect(stream2.data).to.contains('Simple test'); + }); + }); + + it('Should wait until reporter stream is finished (GH-2502)', function () { + const stream = createAsyncTestStream(); + + const runOpts = { + only: ['chrome'], + reporter: [ + { + name: 'json', + output: stream, + }, + ], + }; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', runOpts) + .then(() => { + expect(stream.finalCalled).to.be.ok; + }); + }); + + it('Should wait until reporter stream failed to finish (GH-2502)', function () { + const stream = createAsyncTestStream({ shouldFail: true }); + + const runOpts = { + only: ['chrome'], + reporter: [ + { + name: 'json', + output: stream, + }, + ], + }; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', runOpts) + .then(() => { + expect(stream.finalCalled).to.be.ok; + }); + }); + + it('Should not close stdout when it is specified as a reporter stream (GH-3114)', function () { + let streamFinished = false; + + process.stdout.write = () => { + process.stdout.write = stdoutWrite; + }; + + process.stdout.on('finish', () => { + streamFinished = false; + }); + + const runOpts = { + only: ['chrome'], + reporter: [ + { + name: 'json', + output: process.stdout, + }, + ], + }; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', runOpts) + .then(() => { + process.stdout.write = stdoutWrite; + + expect(streamFinished).to.be.not.ok; + }); + }); + + it('Should not close stderr when it is specified as a reporter stream (GH-3114)', function () { + let streamFinished = false; + + process.stderr.write = () => { + process.stderr.write = stderrWrite; + }; + + process.stderr.on('finish', () => { + streamFinished = false; + }); + + const runOpts = { + only: ['chrome'], + reporter: [ + { + name: 'json', + output: process.stderr, + }, + ], + }; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', runOpts) + .then(() => { + process.stderr.write = stderrWrite; + + expect(streamFinished).to.be.not.ok; + }); + }); + + it('Should not close stdout when undefined is specified as a reporter stream (GH-3114)', function () { + let streamFinished = false; + + process.stdout.write = () => { + process.stdout.write = stdoutWrite; + }; + + process.stdout.on('finish', () => { + streamFinished = false; + }); + + const runOpts = { + only: ['chrome'], + reporter: [ + { + name: 'json', + output: void 0, + }, + ], + }; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', runOpts) + .then(() => { + expect(streamFinished).to.be.not.ok; + }); + }); + + it('Should not close tty streams (GH-3114)', function () { + const stream = createAsyncTestStream({ shouldFail: true }); + + stream.isTTY = true; + + const runOpts = { + only: ['chrome'], + reporter: [ + { + name: 'json', + output: stream, + }, + ], + }; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', runOpts) + .then(() => { + expect(stream.finalCalled).to.be.not.ok; + }); + }); + + it('Should support filename as reporter output', () => { + const testStream = createSimpleTestStream(); + const reportFileName = 'list.report'; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', { + only: ['chrome'], + reporter: [ + { + name: 'list', + output: testStream, + }, + { + name: 'list', + output: reportFileName, + }, + ], + }) + .then(() => { + const reportDataFromFile = fs.readFileSync(reportFileName).toString(); + + expect(testStream.data).eql(reportDataFromFile); + + fs.unlinkSync(reportFileName); + }); + }); + + it('Should work with streams that emit the "finish" event synchronously (GH-3209)', function () { + const stream = createSyncTestStream(); + + const runOpts = { + only: ['chrome'], + + reporter: [ + { + name: 'json', + output: stream, + }, + ], + }; + + return runTests('testcafe-fixtures/index-test.js', 'Simple test', runOpts) + .then(() => { + expect(stream.finalCalled).to.be.ok; + }); + }); + + it('reportTestActionDone should work in the raw tests', () => { + const expected = [ + { expression: 'Selector(\'#button\')', element: { tagName: 'button', attributes: { type: 'button', id: 'button' } } }, + ]; + + function customReporter (log) { + return createReporter({ + reportTestActionDone: async (name, { command, browser }) => { + log[browser.alias] = log[browser.alias] || []; + + if (command.selector) + log[browser.alias].push(command.selector); + }, + }); + } + + const log = {}; + + return runTests('testcafe-fixtures/index-test.testcafe', 'reportTestActionDone should work in the raw tests', { reporter: customReporter(log) }) + .then(() => { + const logs = Object.values(log); + + expect(logs.length).gt(0); + + logs.forEach(browserLog => expect(browserLog).eql(expected)); + }); + }); + + describe('Test actions', () => { + function generateRunOptions (log, options) { + return { + only: ['chrome'], + disableScreenshots: true, + reporter: generateReporter(log, options), + }; + } + + let log = null; + + beforeEach(() => { + log = []; + }); + + it('Simple command', function () { + return runTests('testcafe-fixtures/index-test.js', 'Simple command test', generateRunOptions(log, { + includeBrowserInfo: true, + includeTestInfo: true, + })) + .then(() => { + expect(log).eql([ + { + name: 'click', + action: 'start', + browser: 'chrome', + test: { + id: 'test-id', + name: 'Simple command test', + phase: 'inTest', + }, + fixture: { + id: 'fixture-id', + name: 'Reporter', + }, + }, + { + name: 'click', + action: 'done', + command: { + type: 'click', + selector: 'Selector(\'#target\')', + options: { + offsetX: 10, + }, + }, + test: { + id: 'test-id', + name: 'Simple command test', + phase: 'inTest', + }, + fixture: { + id: 'fixture-id', + name: 'Reporter', + }, + }, + ]); + }); + }); + + it('Simple command Error', function () { + return runTests('testcafe-fixtures/index-test.js', 'Simple command err test', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { + name: 'click', + action: 'start', + }, + { + name: 'click', + action: 'done', + command: { + type: 'click', + selector: 'Selector(\'#non-existing-target\')', + }, + err: 'E24', + }, + ]); + }); + }); + + it('Simple assertion', function () { + return runTests('testcafe-fixtures/index-test.js', 'Simple assertion', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { + name: 'eql', + action: 'start', + }, + { + name: 'eql', + action: 'done', + command: { + type: 'assertion', + actual: true, + assertionType: 'eql', + expected: true, + expected2: void 0, + message: 'assertion message', + options: { + timeout: 100, + }, + }, + }, + ]); + }); + }); + + it('Selector assertion', function () { + return runTests('testcafe-fixtures/index-test.js', 'Selector assertion', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { + name: 'eql', + action: 'start', + }, + { + name: 'eql', + action: 'done', + command: { + type: 'assertion', + actual: 'target', + assertionType: 'eql', + expected: 'target', + expected2: void 0, + message: null, + }, + }, + ]); + }); + }); + + it('Snapshot', function () { + return runTests('testcafe-fixtures/index-test.js', 'Snapshot', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { + name: 'execute-selector', + action: 'start', + }, + { + name: 'execute-selector', + action: 'done', + command: { + selector: 'Selector(\'#target\')', + type: 'execute-selector', + }, + }, + { + name: 'execute-selector', + action: 'start', + }, + { + name: 'execute-selector', + action: 'done', + command: { + selector: 'Selector(\'body\').find(\'#target\')', + type: 'execute-selector', + }, + }, + ]); + }); + }); + + it('Client Function', function () { + return runTests('testcafe-fixtures/index-test.js', 'Client Function', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { name: 'execute-client-function', action: 'start' }, + { + name: 'execute-client-function', + action: 'done', + command: { + clientFn: { + args: [ + 1, + true, + ], + code: '(function(){ var func = function func(bool) {return function () {return bool;};}; return func;})();', + }, + type: 'execute-client-function', + }, + }] + ); + }); + }); + + it('Complex command', function () { + return runTests('testcafe-fixtures/index-test.js', 'Complex command test', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { name: 'useRole', action: 'start' }, + { + name: 'useRole', + action: 'done', + command: { + role: { + loginUrl: 'http://localhost:3000/fixtures/reporter/pages/index.html', + options: { preserveUrl: true }, + phase: 'initialized', + }, + type: 'useRole', + }, + }, + ]); + }); + }); + + it('Complex nested command', function () { + return runTests('testcafe-fixtures/index-test.js', 'Complex nested command test', generateRunOptions(log, { includeTestInfo: true })) + .then(() => { + expect(log).eql([ + { + name: 'useRole', + action: 'start', + test: { + id: 'test-id', + name: 'Complex nested command test', + phase: 'inTest', + }, + fixture: { + id: 'fixture-id', + name: 'Reporter', + }, + }, + { + name: 'click', + action: 'start', + test: { + id: 'test-id', + name: 'Complex nested command test', + phase: 'inRoleInitializer', + }, + fixture: { + id: 'fixture-id', + name: 'Reporter', + }, + }, + { + name: 'click', + action: 'done', + command: { + selector: 'Selector(\'#target\')', + type: 'click', + }, + test: { + id: 'test-id', + name: 'Complex nested command test', + phase: 'inRoleInitializer', + }, + fixture: { + id: 'fixture-id', + name: 'Reporter', + }, + }, + { + name: 'useRole', + action: 'done', + command: { + role: { + loginUrl: 'http://localhost:3000/fixtures/reporter/pages/index.html', + options: { 'preserveUrl': false }, + phase: 'initialized', + }, + type: 'useRole', + }, + test: { + id: 'test-id', + name: 'Complex nested command test', + phase: 'inTest', + }, + fixture: { + id: 'fixture-id', + name: 'Reporter', + }, + }, + ]); + }); + }); + + it('Complex nested command error', function () { + return runTests('testcafe-fixtures/index-test.js', 'Complex nested command error', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { name: 'useRole', action: 'start' }, + { name: 'click', action: 'start' }, + { + name: 'click', + action: 'done', + command: { + selector: 'Selector(\'#non-existing-element\')', + type: 'click', + }, + err: 'E24', + }, + { + name: 'useRole', + action: 'done', + command: { + role: { + loginUrl: 'http://localhost:3000/fixtures/reporter/pages/index.html', + options: { 'preserveUrl': false }, + phase: 'initialized', + }, + type: 'useRole', + }, + }, + ]); + }); + }); + + it('Eval', function () { + return runTests('testcafe-fixtures/index-test.js', 'Eval', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { name: 'execute-client-function', action: 'start' }, + { + name: 'execute-client-function', + action: 'done', + command: { + clientFn: { + args: [], + code: '(function(){ var func = function func() {return document.getElementById(\'#target\');}; return func;})();', + }, + type: 'execute-client-function', + }, + }, + ]); + }); + }); + + it('Should not add action information in report if action was emitted after test done (GH-5650)', () => { + return runTests('testcafe-fixtures/index-test.js', 'Action done after test done', generateRunOptions(log)) + .then(() => { + const EXPECTED_LOG = [ + { name: 'execute-client-function', action: 'start' }, + { name: 'wait', action: 'start' }, + { + name: 'execute-client-function', + action: 'done', + command: { + type: 'execute-client-function', + clientFn: { + code: '(function(){ var func = function func() {return __get$Loc(location) .reload(true);}; return func;})();', + args: [], + }, + }, + }, + ]; + + expect(log).eql(EXPECTED_LOG); + }); + }); + + it('Value typed using the "typeText" action in the input[type=password] should be masked', () => { + return runTests( + 'testcafe-fixtures/index-test.js', + 'The "typeText" action with the input[type=password]', + { reporter: generateReporter(log, { includeCommandInfo: true }) } + ).then(() => { + expect(log).to.include.deep.members([ + { + name: 'typeText', + action: 'start', + command: { + type: 'type-text', + selector: { + expression: "Selector('#password-input')", + }, + options: { + confidential: true, + }, + text: '********', + }, + }, + { + name: 'typeText', + action: 'done', + command: { + type: 'type-text', + selector: "Selector('#password-input')", + options: { + confidential: true, + }, + text: '********', + }, + }, + ]); + }); + }); + + it('Value typed using the "typeText" action should be masked if "confidential" flag is set to true', () => { + return runTests( + 'testcafe-fixtures/index-test.js', + 'The "typeText" action with the input[type=text] and the "confidential" flag set to true', + { reporter: generateReporter(log, { includeCommandInfo: true }) }, + ).then(() => { + expect(log).to.include.deep.members([ + { + name: 'typeText', + action: 'start', + command: { + type: 'type-text', + selector: { + expression: "Selector('#input')", + }, + options: { + confidential: true, + }, + text: '********', + }, + }, + { + name: 'typeText', + action: 'done', + command: { + type: 'type-text', + selector: "Selector('#input')", + options: { + confidential: true, + }, + text: '********', + }, + }, + ]); + }); + }); + + it('Value typed using the "typeText" action shouldn\'t be masked if "confidential" flag is set to false', () => { + return runTests( + 'testcafe-fixtures/index-test.js', + 'The "typeText" action with the input[type=password] and the "confidential" flag set to false', + { reporter: generateReporter(log, { includeCommandInfo: true }) } + ).then(() => { + expect(log).to.include.deep.members([ + { + name: 'typeText', + action: 'start', + command: { + type: 'type-text', + selector: { + expression: "Selector('#password-input')", + }, + options: { + confidential: false, + }, + text: 'pa$$w0rd', + }, + }, + { + name: 'typeText', + action: 'done', + command: { + type: 'type-text', + selector: "Selector('#password-input')", + options: { + confidential: false, + }, + text: 'pa$$w0rd', + }, + }, + ]); + }); + }); + + it('Value typed using the "pressKey" action in the input[type=password] should be masked', () => { + return runTests( + 'testcafe-fixtures/index-test.js', + 'The "pressKey" action with the input[type=password]', + { reporter: generateReporter(log, { includeCommandInfo: true }) } + ).then(() => { + expect(log).to.include.deep.members([ + { + name: 'pressKey', + action: 'start', + command: { + type: 'press-key', + options: { + confidential: true, + }, + keys: '********', + }, + }, + { + name: 'pressKey', + action: 'done', + command: { + type: 'press-key', + options: { + confidential: true, + }, + keys: '********', + }, + }, + ]); + }); + }); + + it('Value typed using the "pressKey" action should be masked if "confidential" flag is set to true', () => { + return runTests( + 'testcafe-fixtures/index-test.js', + 'The "pressKey" action with the input[type=text] and the "confidential" flag set to true', + { reporter: generateReporter(log, { includeCommandInfo: true }) } + ).then(() => { + expect(log).to.include.deep.members([ + { + name: 'pressKey', + action: 'start', + command: { + type: 'press-key', + options: { + confidential: true, + }, + keys: '********', + }, + }, + { + name: 'pressKey', + action: 'done', + command: { + type: 'press-key', + options: { + confidential: true, + }, + keys: '********', + }, + }, + ]); + }); + }); + + it('Value typed using the "pressKey" action shouldn\'t be masked if "confidential" flag is set to false', () => { + return runTests( + 'testcafe-fixtures/index-test.js', + 'The "pressKey" action with the input[type=password] and the "confidential" flag set to false', + { reporter: generateReporter(log, { includeCommandInfo: true }) } + ).then(() => { + expect(log).to.include.deep.members([ + { + name: 'pressKey', + action: 'start', + command: { + type: 'press-key', + options: { + confidential: false, + }, + keys: 'p a $ $ w 0 r d enter', + }, + }, + { + name: 'pressKey', + action: 'done', + command: { + type: 'press-key', + options: { + confidential: false, + }, + keys: 'p a $ $ w 0 r d enter', + }, + }, + ]); + }); + }); + + it('Should repeat role error in each tests', function () { + return runTests('testcafe-fixtures/index-test.js', 'Repeated role error', generateRunOptions(log)) + .then(() => { + expect(log).eql([ + { name: 'useRole', action: 'start' }, + { name: 'click', action: 'start' }, + { + name: 'click', + action: 'done', + command: { + selector: 'Selector(\'#non-existing-element\')', + type: 'click', + }, + err: 'E24', + }, + { + name: 'useRole', + action: 'done', + command: { + role: { + loginUrl: 'http://localhost:3000/fixtures/reporter/pages/index.html', + options: { 'preserveUrl': false }, + phase: 'initialized', + }, + type: 'useRole', + }, + }, + { name: 'useRole', action: 'start' }, + { + name: 'useRole', + action: 'done', + command: { + role: { + loginUrl: 'http://localhost:3000/fixtures/reporter/pages/index.html', + options: { 'preserveUrl': false }, + phase: 'initialized', + }, + type: 'useRole', + }, + err: 'E24', + }, + ]); + }); + }); + }); + + describe('Report Data', () => { + let reportDataInfos = {}; + let testDoneInfos = {}; + let reporter = null; + + const createReportDataReporter = () => { + return createReporter({ + reportData: ({ browser }, ...data) => { + const alias = browser.alias; + + if (!reportDataInfos[alias]) + reportDataInfos[alias] = []; + + reportDataInfos[alias].push(data); + }, + reportTestDone: (name, { reportData, browsers }) => { + browsers.forEach(({ testRunId, alias }) => { + testDoneInfos[alias] = reportData[testRunId]; + }); + }, + }); + }; + + beforeEach(() => { + reportDataInfos = {}; + testDoneInfos = {}; + + reporter = createReportDataReporter(); + }); + + it('Should raise "reportData" event', async () => { + const expectedReportData = [1, true, 'string', { 'reportResult': 'test' }]; + + await runTests('testcafe-fixtures/report-data-test.js', 'Run t.report action', { + reporter, + }); + + const reportDataBrowserInfos = Object.entries(reportDataInfos); + const testDoneBrowserInfos = Object.entries(testDoneInfos); + + expect(reportDataBrowserInfos.length).eql(config.browsers.length); + expect(testDoneBrowserInfos.length).eql(config.browsers.length); + + reportDataBrowserInfos.forEach(([alias, reportData]) => { + expect(reportData.flat()).eql(testDoneInfos[alias]); + }); + + testDoneBrowserInfos.forEach(([, reportData]) => { + const [, ...rest] = reportData; + + expect(rest).eql(expectedReportData); + }); + }); + }); + + describe('Warnings', () => { + let warningResult = {}; + let reporter = null; + let assertReporterWarnings = null; + + beforeEach(() => { + ({ reporter, assertReporterWarnings, warningResult } = createWarningReporter()); + }); + + it('Should get warning for TestRun', async () => { + try { + await runTests('testcafe-fixtures/index-test.js', 'Asynchronous method', { + reporter, + shouldFail: true, + }); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(warningResult.warnings[0].message).to.include("An asynchronous method that you do not await includes an assertion. Inspect that method's execution chain and add the 'await' keyword where necessary."); + expect(warningResult.warnings[0].testRunId).to.be.a('string'); + expect(warningResult.warnings[0].testRunId).to.not.empty; + + assertReporterWarnings('ok'); + } + }); + + if (config.useLocalBrowsers) { + it('Should get warning for Task', async () => { + try { + await runTests('./testcafe-fixtures/index-test.js', 'Take screenshots with same path', { + setScreenshotPath: true, + shouldFail: true, + reporter, + }); + + throw new Error('Promise rejection expected'); + } + catch (err) { + const SCREENSHOTS_PATH = path.resolve(assertionHelper.SCREENSHOTS_PATH); + const screenshotFileName = path.join(SCREENSHOTS_PATH, '1.png'); + + expect(warningResult.warnings[0].message).to.include( + `The file at "${screenshotFileName}" already exists. It has just been rewritten ` + + 'with a recent screenshot. This situation can possibly cause issues. To avoid them, make sure ' + + 'that each screenshot has a unique path. If a test runs in multiple browsers, consider ' + + 'including the user agent in the screenshot path or generate a unique identifier in another way.', + ); + } + + await assertionHelper.removeScreenshotDir(); + }); + } + + // NOTE: the `event.isSameOriginPolicyFailed` property return `false` in the nativeAutomation mode, + // but in the proxy mode it returns `true` + // the problem is only with warning log, but not with request mock + skipInNativeAutomation('Should get warning for request hook', async () => { + await runTests('./testcafe-fixtures/failed-cors-validation.js', 'Failed CORS validation', { + only: 'chrome', + reporter, + }); + + expect(warningResult.warnings[0].message).to.include('RequestMock: CORS validation failed for a request specified as { url: "http://dummy-url.com/get" }'); + }); + + it('Should get warning for request hook', async () => { + await runTests('./testcafe-fixtures/index-test.js', 'Simple test', { + only: 'chrome', + reporter, + tsConfigPath: 'path-to-ts-config', + }); + + expect(warningResult.warnings[0].message).to.eql("The 'tsConfigPath' option is deprecated and will be removed in the next major release. Use the 'compilerOptions.typescript.configPath' option instead."); + }); + }); + + describe('Action snapshots', () => { + it('Basic', () => { + const expected = [ + { expression: 'Selector(\'#input\')', timeout: 11000, element: { tagName: 'input', attributes: { value: '100', type: 'text', id: 'input' } } }, + { expression: 'Selector(\'#obscuredInput\')', element: { tagName: 'div', attributes: { id: 'fixed' } } }, + { expression: 'Selector(\'#obscuredInput\')', element: { tagName: 'div', attributes: { id: 'fixed' } } }, + { expression: 'Selector(\'#obscuredDiv\')', element: { tagName: 'div', attributes: { id: 'obscuredDiv' } } }, + { expression: 'Selector(\'#p1\')', element: { attributes: { id: 'p1' }, tagName: 'p' } }, + { expression: 'Selector(\'#p2\')', element: { attributes: { id: 'p2' }, tagName: 'p' } }, + ]; + + function customReporter (log) { + return () => { + return { + async reportTestActionDone (name, { command, browser }) { + log[browser.alias] = log[browser.alias] || []; + + if (command.selector) + log[browser.alias].push(command.selector); + + if (command.startSelector) + log[browser.alias].push(command.startSelector); + + if (command.endSelector) + log[browser.alias].push(command.endSelector); + + if (command.destinationSelector) + log[browser.alias].push(command.destinationSelector); + }, + async reportTaskStart () { + }, + async reportFixtureStart () { + }, + async reportTestDone () { + }, + async reportTaskDone () { + }, + }; + }; + } + + const log = {}; + + return runTests('testcafe-fixtures/snapshots-test.js', 'Basic', { reporter: customReporter(log) }) + .then(() => { + const logs = Object.values(log); + + expect(logs.length).gt(0); + + logs.forEach(browserLog => expect(browserLog).eql(expected)); + }); + }); + + it('Full snapshot', () => { + function customReporter (log) { + return () => { + return { + async reportTestActionDone (name, { command, browser }) { + log[browser.alias] = log[browser.alias] || []; + + if (command.selector) + log[browser.alias].push(command.selector); + }, + async reportTaskStart () { + }, + async reportFixtureStart () { + }, + async reportTestDone () { + }, + async reportTaskDone () { + }, + }; + }; + } + + const log = {}; + + return runTests('testcafe-fixtures/snapshots-test.js', 'Full snapshot', { reporter: customReporter(log) }) + .then(() => { + const logs = Object.values(log); + + expect(logs.length).gt(0); + + logs.forEach(browserLog => { + expect(browserLog[0].expression).eql('Selector(\'#input\')'); + expect(browserLog[0].element.tagName).eql('input'); + expect(browserLog[0].element.attributes.type).eql('text'); + }); + }); + }); + }); + + describe('Screenshot errors', () => { + it('Screenshot on action error', () => { + let testDoneErrors = null; + const actionDoneErrors = []; + + function screenshotReporter () { + return { + async reportTestActionDone (name, { err }) { + actionDoneErrors.push(err); + }, + async reportTaskStart () { + }, + async reportFixtureStart () { + }, + async reportTestDone (name, testRunInfo) { + testDoneErrors = testRunInfo.errs; + }, + async reportTaskDone () { + }, + }; + } + + return runTests('testcafe-fixtures/index-test.js', 'Screenshot on action error', { + only: 'chrome', + reporter: screenshotReporter, + screenshotsOnFails: true, + }) + .then(() => { + expect(actionDoneErrors[0]).is.undefined; + expect(actionDoneErrors[1].code).eql('E24'); + expect(testDoneErrors.map(err => err.code)).eql(['E24', 'E8']); + expect(testDoneErrors[0].screenshotPath).is.not.empty; + expect(testDoneErrors[0].screenshotPath).eql(testDoneErrors[1].screenshotPath); + + return assertionHelper.removeScreenshotDir('screenshots'); + }); + }); + + it('Should put actionId on screenshot information', async () => { + function customReporter (actionIds, screenshots) { + return createReporter({ + reportTestActionDone: async (name, { command }) => { + actionIds.push(command.actionId); + }, + + reportTestDone: async (name, testRunInfo) => { + testRunInfo.screenshots.forEach((screenshot) => { + screenshots[screenshot.actionId] = screenshot.screenshotPath; + }); + }, + }); + } + + const actionIds = []; + const screenshots = {}; + + await runTests('./testcafe-fixtures/index-test.js', 'Take a screenshot on action and on error', { + only: 'chrome', + reporter: customReporter(actionIds, screenshots), + screenshotsOnFails: true, + }); + + expect(actionIds.length).eql(2); + actionIds.forEach(actionId => { + expect(screenshots[actionId]).is.not.empty; + }); + + assertionHelper.removeScreenshotDir('screenshots'); + }); + + }); + + describe('Reporter Hooks', () => { + it('Should handle onBeforeWrite hook', () => { + const writeData = { prop: true }; + const result = {}; + const expectedOutput = `formattedText\nformattedText\nformattedText\n`; + const expectedFormatOptions = { indent: 3, useWordWrap: true }; + const expectedResult = { + reportTaskStart: { + data: writeData, + formatOptions: expectedFormatOptions, + }, + reportTestDone: { + data: writeData, + formatOptions: expectedFormatOptions, + }, + customMethod: { + data: { ...writeData, initiator: 'customMethod' }, + formatOptions: expectedFormatOptions, + }, + }; + + function custom () { + return { + reportTaskDone: noop, + reportFixtureStart: noop, + reportTaskStart () { + this.useWordWrap(true).setIndent(3).write('unFormattedText', writeData); + }, + reportTestDone: function () { + this.write('unFormattedText', writeData); + this.customMethod(); + }, + customMethod () { + this.write('unFormattedText', { ...writeData, initiator: 'customMethod' }); + }, + }; + } + + const onBeforeWriteHook = function (writeInfo) { + const { initiator, data, formatOptions } = writeInfo; + + writeInfo.formattedText = 'formattedText\n'; + result[initiator] = { + data, + formatOptions, + }; + }; + const outStream = createSimpleTestStream(); + + return runTests('testcafe-fixtures/reporter-init-method.js', null, { + reporter: [{ name: custom, output: outStream }], + hooks: { + reporter: { + onBeforeWrite: { + 'custom': onBeforeWriteHook, + }, + }, + }, + }) + .then(() => { + expect(result).eql(expectedResult); + expect(outStream.data).eql(expectedOutput); + }); + + }); + it('Should throw an error if hooks.reporter option is not of object type', async () => { + try { + await runTests('testcafe-fixtures/reporter-init-method.js', null, { + shouldFail: true, + hooks: { + reporter: { + onBeforeWrite: 'stringTypeHook', + }, + }, + }); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql(`Cannot prepare tests due to the following error:\n\n` + + `The reporter.onBeforeWrite (string) is not of expected type (non-null object).`); + } + + }); + it('Should throw an error if onBeforeWrite hook is not of function type', async () => { + try { + await runTests('testcafe-fixtures/reporter-init-method.js', null, { + shouldFail: true, + hooks: { + reporter: { + onBeforeWrite: { + 'custom': 'stringTypeHook', + }, + }, + }, + }); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql(`Cannot prepare tests due to the following error:\n\n` + + `The reporter.onBeforeWrite.custom (string) is not of expected type (function).`); + } + + }); + }); + + it('Should call the "init" method of reporters if it\'s defined', function () { + const reportInitSuccess = result => createReporter({ + init (version) { + result.init = result.init || []; + result.version = version; + + result.init.push(true); + }, + reportTaskDone () { + result.done = result.done || []; + + result.done.push(true); + }, + }); + + const reportNoInit = result => createReporter({ + reportTaskDone () { + result.done = result.done || []; + + result.done.push(true); + }, + }); + + const result = {}; + + return runTests('testcafe-fixtures/reporter-init-method.js', null, { + reporter: [reportInitSuccess(result), reportNoInit(result)], + }) + .then(() => { + expect(result.init).eql([true]); + expect(result.done).eql([true, true]); + expect(result.version).eql(getTestCafeVersion()); + }); + }); + + // NOTE: this test hangs in nativeAutomation for unknown reasons + skipInNativeAutomation('Should raise an error when uncaught exception occurred in any reporter method', async () => { + function createReporterWithBrokenMethod (method) { + const base = { + async reportTaskStart () {}, + async reportFixtureStart () {}, + async reportTestDone () {}, + async reportTaskDone () {}, + }; + + base[method] = () => { + throw new Error('oops'); + }; + + return () => base; + } + + for (const method of Object.values(ReporterPluginMethod)) { + try { + await runTests( + 'testcafe-fixtures/index-test.js', + 'Simple test', + { + reporter: createReporterWithBrokenMethod(method), + shouldFail: true, + tsConfigPath: 'path-to-ts-config', + }, + ); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).startsWith(`The "${method}" method of the "function () {}" reporter produced an uncaught error. Error details:\nError: oops`); + } + } + }); + + it('Should work with option from configuration file', function () { + + return runTestsWithConfig('Simple test', './test/functional/fixtures/reporter/configs/xunit-config.js') + .then(() => { + const pathReport = path.resolve(__dirname, 'report.xml'); + const report = fs.readFileSync(pathReport).toString(); + + expect(report).contains(''); + + del(pathReport); + }); + }); + + it('Should set options _hasTaskErrors to the runner if an error occurs', async () => { + try { + await runTests('testcafe-fixtures/index-test.js', 'Simple command err test', { only: ['chrome'], shouldFail: true }); + } + catch (err) { + expect(testCafe.runner._hasTaskErrors).eql(true); + } + }); + + it('[gh-7731] Should pass duration 0 for skipped tests', function () { + const reportTestDoneReporter = result => createReporter({ + reportTestDone (name, { durationMs, skipped }) { + result.skipped = result.skipped || []; + result.nonSkipped = result.nonSkipped || []; + + if (skipped) + result.skipped.push(durationMs); + else + result.nonSkipped.push(durationMs); + }, + }); + + const result = {}; + + return runTests('testcafe-fixtures/skipped-tests.js', null, { + reporter: [reportTestDoneReporter(result)], + }) + .then(() => { + expect(result.skipped.length).eql(4); + expect(result.nonSkipped.length).eql(1); + + result.skipped.forEach(dur => expect(dur).eql(0)); + result.nonSkipped.forEach(dur => expect(dur).gt(0)); + }); + }); +}); diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/failed-cors-validation.js b/test/functional/fixtures/reporter/testcafe-fixtures/failed-cors-validation.js new file mode 100644 index 00000000000..fb1b475ddce --- /dev/null +++ b/test/functional/fixtures/reporter/testcafe-fixtures/failed-cors-validation.js @@ -0,0 +1,15 @@ +import { Selector, RequestMock } from 'testcafe'; + +const mock = RequestMock() + .onRequestTo('http://dummy-url.com/get') + .respond({ prop: 'value' }, 200, { 'not-specify-cors-headers': true }); + +fixture `Failed CORS validation` + .page('http://localhost:3000/fixtures/api/es-next/request-hooks/pages/request-mock/failed-cors-validation.html') + .requestHooks(mock); + +test('Failed CORS validation', async t => { + await t + .click('#btnSendFetch') + .expect(Selector('#requestStatusText').textContent).eql('CORS failed'); +}); diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/index-test.js b/test/functional/fixtures/reporter/testcafe-fixtures/index-test.js new file mode 100644 index 00000000000..3e30a8a3e27 --- /dev/null +++ b/test/functional/fixtures/reporter/testcafe-fixtures/index-test.js @@ -0,0 +1,141 @@ +import { Role, Selector, ClientFunction } from 'testcafe'; + +const page = 'http://localhost:3000/fixtures/reporter/pages/index.html'; + +fixture `Reporter` + .page `http://localhost:3000/fixtures/reporter/pages/index.html`; + +const simpleRole1 = Role(page, () => { +}, { preserveUrl: true }); + +const complexRole = Role(page, async t => { + await t.click('#target'); +}); + +const errorRole = Role(page, async t => { + await t.click(Selector('#non-existing-element', { timeout: 100 })); +}); + +const foo = ClientFunction(bool => () => bool); + +async function errorCheck (t) { + await new Promise(r => setTimeout(r, 100)); + await t.expect(false).ok(); +} + +test('Simple test', async t => { + await t.wait(1); + await t.report(); +}); + +test('Simple command test', async t => { + await t.click(Selector('#target'), { offsetX: 10 }); +}); + +test('Simple command err test', async t => { + await t.click('#non-existing-target'); +}); + +test('Complex command test', async t => { + await t.useRole(simpleRole1); +}); + +test('Complex nested command test', async t => { + await t.useRole(complexRole); +}); + +test('Complex nested command error', async t => { + await t.useRole(errorRole); +}); + +test('Simple assertion', async t => { + await t.expect(true).eql(true, 'assertion message', { timeout: 100 }); +}); + +test('Selector assertion', async t => { + await t.expect(Selector('#target').innerText).eql('target'); +}); + +test('Snapshot', async () => { + await Selector('#target')(); + + await Selector('body').find('#target').innerText; +}); + +test('Client Function', async () => { + await foo(1, true); +}); + +test('Eval', async t => { + await t.eval(() => document.getElementById('#target')); +}); + +test('Screenshot on action error', async t => { + t.hover('body'); + + await t.click('#unexisting-element'); +}); + +test('Take a screenshot on action and on error', async t => { + await t + .takeScreenshot() + .click('#unexisting-element'); +}); + +test('Action done after test done', async t => { + await t + .wait(5000) + .eval(() => location.reload(true)); +}); + +test('The "typeText" action with the input[type=password]', async t => { + await t.typeText('#password-input', 'pa$$w0rd'); +}); + +test('The "typeText" action with the input[type=text] and the "confidential" flag set to true', async t => { + await t.typeText('#input', 'pa$$w0rd', { confidential: true }); +}); + +test('The "typeText" action with the input[type=password] and the "confidential" flag set to false', async t => { + await t.typeText('#password-input', 'pa$$w0rd', { confidential: false }); +}); + +test('The "pressKey" action with the input[type=password]', async t => { + await t + .click('#password-input') + .pressKey('p a $ $ w 0 r d enter'); +}); + +test('The "pressKey" action with the input[type=text] and the "confidential" flag set to true', async t => { + await t + .click('#input') + .pressKey('p a $ $ w 0 r d enter', { confidential: true }); +}); + +test('The "pressKey" action with the input[type=password] and the "confidential" flag set to false', async t => { + await t + .click('#password-input') + .pressKey('p a $ $ w 0 r d enter', { confidential: false }); +}); + +test('Asynchronous method', async t => { + errorCheck(t); +}); + +test('Asynchronous method', async t => { + await errorCheck(t); +}); + +test('Take screenshots with same path', async t => { + await t + .takeScreenshot('1.png') + .takeScreenshot('1.png'); +}); + +test('Repeated role error', async t => { + await t.useRole(errorRole); +}); + +test('Repeated role error', async t => { + await t.useRole(errorRole); +}); diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/index-test.testcafe b/test/functional/fixtures/reporter/testcafe-fixtures/index-test.testcafe new file mode 100644 index 00000000000..bcfdddaa212 --- /dev/null +++ b/test/functional/fixtures/reporter/testcafe-fixtures/index-test.testcafe @@ -0,0 +1,26 @@ +{ + "fixtures": [ + { + "name": "Reporter", + "pageUrl": "http://localhost:3000/fixtures/reporter/pages/index.html", + "tests": [ + { + "name": "reportTestActionDone should work in the raw tests", + "commands": [ + { + "type": "click", + "selector": { + "type": "js-expr", + "value": "Selector('#button')" + }, + "options": { + "offsetX": 10, + "offsetY": 10 + } + } + ] + } + ] + } + ] +} diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/report-data-test.js b/test/functional/fixtures/reporter/testcafe-fixtures/report-data-test.js new file mode 100644 index 00000000000..8bbded0e186 --- /dev/null +++ b/test/functional/fixtures/reporter/testcafe-fixtures/report-data-test.js @@ -0,0 +1,8 @@ +fixture`Report Data API` + .page('../pages/index.html'); + +test('Run t.report action', async t => { + await t + .report(t.browser.alias) + .report(1, true, 'string', { 'reportResult': 'test' }); +}); diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/reporter-init-method.js b/test/functional/fixtures/reporter/testcafe-fixtures/reporter-init-method.js new file mode 100644 index 00000000000..6368625045d --- /dev/null +++ b/test/functional/fixtures/reporter/testcafe-fixtures/reporter-init-method.js @@ -0,0 +1,6 @@ +fixture `Reporter init method` + .page('http://localhost:3000/fixtures/reporter/pages/index.html'); + +test(`test`, async t => { + await t.wait(1); +}); diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/skipped-tests.js b/test/functional/fixtures/reporter/testcafe-fixtures/skipped-tests.js new file mode 100644 index 00000000000..2fb8d802acb --- /dev/null +++ b/test/functional/fixtures/reporter/testcafe-fixtures/skipped-tests.js @@ -0,0 +1,18 @@ +fixture `Skipped tests` + .page `http://localhost:3000/fixtures/reporter/pages/index.html`; +test('Simple test', async t => { + await t.wait(1); + await t.report(); +}); +test.skip('Skipped test 1', async t => { + await t.click('#non-existing-target'); +}); + +test.skip('Skipped test 2', async () => { +}); + +test.skip('Skipped test 3', async () => { +}); + +test.skip('Skipped test 4', async () => { +}); diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/snapshots-test.js b/test/functional/fixtures/reporter/testcafe-fixtures/snapshots-test.js new file mode 100644 index 00000000000..bd09d2c498d --- /dev/null +++ b/test/functional/fixtures/reporter/testcafe-fixtures/snapshots-test.js @@ -0,0 +1,15 @@ +import { Selector } from 'testcafe'; + +fixture `Reporter snapshots` + .page `http://localhost:3000/fixtures/reporter/pages/snapshots.html`; + +test('Basic', async t => { + await t.click(Selector('#input', { timeout: 11000 })); + await t.click('#obscuredInput'); + await t.dragToElement('#obscuredInput', '#obscuredDiv'); + await t.selectEditableContent('#p1', '#p2'); +}); + +test('Full snapshot', async () => { + await Selector('#input')(); +}); diff --git a/test/functional/fixtures/request-pipeline/content-security-policy/pages/csp.html b/test/functional/fixtures/request-pipeline/content-security-policy/pages/csp.html new file mode 100644 index 00000000000..8a5847bb9ee --- /dev/null +++ b/test/functional/fixtures/request-pipeline/content-security-policy/pages/csp.html @@ -0,0 +1,12 @@ + + + + + Content Security Policy + + + +

Content Security Policy

+

+ + diff --git a/test/functional/fixtures/request-pipeline/content-security-policy/scripts/script.js b/test/functional/fixtures/request-pipeline/content-security-policy/scripts/script.js new file mode 100644 index 00000000000..fcdf2fbae7a --- /dev/null +++ b/test/functional/fixtures/request-pipeline/content-security-policy/scripts/script.js @@ -0,0 +1,5 @@ +document.addEventListener('DOMContentLoaded', function () { + const h2 = document.getElementsByTagName('h2')[0]; + + h2.textContent = 'script.js is loaded.'; +}); diff --git a/test/functional/fixtures/request-pipeline/content-security-policy/test.js b/test/functional/fixtures/request-pipeline/content-security-policy/test.js new file mode 100644 index 00000000000..ab8cbaed1bf --- /dev/null +++ b/test/functional/fixtures/request-pipeline/content-security-policy/test.js @@ -0,0 +1,5 @@ +describe('Content Security Policy', () => { + it('Should disable CSP rules', () => { + return runTests('testcafe-fixtures/csp.js', null, { only: 'chrome' }); + }); +}); diff --git a/test/functional/fixtures/request-pipeline/content-security-policy/testcafe-fixtures/csp.js b/test/functional/fixtures/request-pipeline/content-security-policy/testcafe-fixtures/csp.js new file mode 100644 index 00000000000..5063c88fb97 --- /dev/null +++ b/test/functional/fixtures/request-pipeline/content-security-policy/testcafe-fixtures/csp.js @@ -0,0 +1,8 @@ +import { Selector } from 'testcafe'; + +fixture `Fixture` + .page `http://localhost:3000/fixtures/request-pipeline/content-security-policy/pages/csp.html`; + +test('test', async t => { + await t.expect(Selector('h2').textContent).eql('script.js is loaded.'); +}); diff --git a/test/functional/fixtures/request-pipeline/errors/test.js b/test/functional/fixtures/request-pipeline/errors/test.js new file mode 100644 index 00000000000..f480da2402a --- /dev/null +++ b/test/functional/fixtures/request-pipeline/errors/test.js @@ -0,0 +1,12 @@ +const { expect } = require('chai'); +const { onlyInNativeAutomation } = require('../../../utils/skip-in'); + +describe('Should handle request pipeline errors', function () { + onlyInNativeAutomation('Certificate error', function () { + return runTests('./testcafe-fixtures/certificate-error.js', null, { shouldFail: true }) + .catch(errs => { + expect(errs[0]).contains('Failed to load the page at "https://localhost:3007/".'); + expect(errs[0]).contains('Error: SSL certificate error (ERR_CERT_AUTHORITY_INVALID)'); + }); + }); +}); diff --git a/test/functional/fixtures/request-pipeline/errors/testcafe-fixtures/certificate-error.js b/test/functional/fixtures/request-pipeline/errors/testcafe-fixtures/certificate-error.js new file mode 100644 index 00000000000..4b84367d8c1 --- /dev/null +++ b/test/functional/fixtures/request-pipeline/errors/testcafe-fixtures/certificate-error.js @@ -0,0 +1,4 @@ +fixture `Certificate error` + .page('https://localhost:3007/'); + +test('test', () => {}); diff --git a/test/functional/fixtures/request-pipeline/redirects/pages/final.html b/test/functional/fixtures/request-pipeline/redirects/pages/final.html new file mode 100644 index 00000000000..e81d8d84775 --- /dev/null +++ b/test/functional/fixtures/request-pipeline/redirects/pages/final.html @@ -0,0 +1,10 @@ + + + + + Final page + + +

Final page

+ + diff --git a/test/functional/fixtures/request-pipeline/redirects/pages/index.html b/test/functional/fixtures/request-pipeline/redirects/pages/index.html new file mode 100644 index 00000000000..c48a36c7a1b --- /dev/null +++ b/test/functional/fixtures/request-pipeline/redirects/pages/index.html @@ -0,0 +1,11 @@ + + + + + Redirects + + +

Redirects

+ Redirect to final.html + + diff --git a/test/functional/fixtures/request-pipeline/redirects/test.js b/test/functional/fixtures/request-pipeline/redirects/test.js new file mode 100644 index 00000000000..17cfb4b1229 --- /dev/null +++ b/test/functional/fixtures/request-pipeline/redirects/test.js @@ -0,0 +1,5 @@ +describe('Redirects', () => { + it('Should correctly handle redirects', () => { + return runTests('testcafe-fixtures/redirects.js', null, { only: 'chrome' }); + }); +}); diff --git a/test/functional/fixtures/request-pipeline/redirects/testcafe-fixtures/redirects.js b/test/functional/fixtures/request-pipeline/redirects/testcafe-fixtures/redirects.js new file mode 100644 index 00000000000..8cd0607df81 --- /dev/null +++ b/test/functional/fixtures/request-pipeline/redirects/testcafe-fixtures/redirects.js @@ -0,0 +1,10 @@ +import { Selector } from 'testcafe'; + +fixture `Fixture` + .page('http://localhost:3000/fixtures/request-pipeline/redirects/pages/index.html'); + +test('test', async t => { + await t + .click('#redirect') + .expect(Selector('h1').textContent).eql('Final page'); +}); diff --git a/test/functional/fixtures/request-pipeline/xhr/pages/index.html b/test/functional/fixtures/request-pipeline/xhr/pages/index.html new file mode 100644 index 00000000000..85e268a4ebb --- /dev/null +++ b/test/functional/fixtures/request-pipeline/xhr/pages/index.html @@ -0,0 +1,75 @@ + + + + XMLHttpRequest + + +
+ + + +
+ + + + diff --git a/test/functional/fixtures/request-pipeline/xhr/test.js b/test/functional/fixtures/request-pipeline/xhr/test.js new file mode 100644 index 00000000000..836ad2ee2b6 --- /dev/null +++ b/test/functional/fixtures/request-pipeline/xhr/test.js @@ -0,0 +1,18 @@ +const { errorInEachBrowserContains } = require('../../../assertion-helper.js'); + +describe('XHR', () => { + it('Should keep header if request was reopened', () => { + return runTests('./testcafe-fixtures/index.js', 'Click test header button'); + }); + + it('Should not return authorization prefix for the authorization header', () => { + return runTests('./testcafe-fixtures/index.js', 'Click auth header button'); + }); + + it('Should wait for xhr-requests after an action', () => { + return runTests('./testcafe-fixtures/index.js', 'Click delay button', { shouldFail: true }) + .catch(errs => { + errorInEachBrowserContains(errs, 'Xhr requests are finished', 0); + }); + }); +}); diff --git a/test/functional/fixtures/request-pipeline/xhr/testcafe-fixtures/index.js b/test/functional/fixtures/request-pipeline/xhr/testcafe-fixtures/index.js new file mode 100644 index 00000000000..1be6124af45 --- /dev/null +++ b/test/functional/fixtures/request-pipeline/xhr/testcafe-fixtures/index.js @@ -0,0 +1,16 @@ +import { Selector } from 'testcafe'; + +fixture('My fixture') + .page('http://localhost:3000/fixtures/request-pipeline/xhr/pages/index.html'); +test('Click test header button', async t => { + await t.click('#test-header'); + await t.expect(Selector('#xhr-result').textContent).eql('test-string'); +}); +test('Click auth header button', async t => { + await t.click('#auth-header'); + await t.expect(Selector('#xhr-result').textContent).eql('authorization-string'); +}); +test('Click delay button', async t => { + await t.click('#delay'); + await t.click('#xhr-result'); +}); diff --git a/test/functional/fixtures/run-options/disable-page-caching/common/index.js b/test/functional/fixtures/run-options/disable-page-caching/common/index.js new file mode 100644 index 00000000000..e1f0cfbced5 --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/common/index.js @@ -0,0 +1,28 @@ +import { + ClientFunction, + Role, + Selector, + t, +} from 'testcafe'; + +const getPageLocation = ClientFunction(() => window.location.toString()); + +const url = 'http://localhost:3000/fixtures/run-options/disable-page-caching/pages/index.html'; + +const expectedRoleLastPageLocation = 'http://localhost:3000/fixtures/run-options/disable-page-caching/pages/third.html'; + +const role = Role(url, async () => { + await t + .click(Selector('#first')) + .click(Selector('#second')) + .click(Selector('#third')); + + role.lastPageLocation = await getPageLocation(); +}); + +export { + role, + url, + expectedRoleLastPageLocation, +}; + diff --git a/test/functional/fixtures/run-options/disable-page-caching/pages/index.html b/test/functional/fixtures/run-options/disable-page-caching/pages/index.html new file mode 100644 index 00000000000..45bc5955331 --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/pages/index.html @@ -0,0 +1,26 @@ + + + + + Title + + + + + + + diff --git a/test/functional/fixtures/run-options/disable-page-caching/pages/second.html b/test/functional/fixtures/run-options/disable-page-caching/pages/second.html new file mode 100644 index 00000000000..f23fce87a73 --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/pages/second.html @@ -0,0 +1,17 @@ + + + + + Title + + + 3. Link to the third page + + + diff --git a/test/functional/fixtures/run-options/disable-page-caching/pages/third.html b/test/functional/fixtures/run-options/disable-page-caching/pages/third.html new file mode 100644 index 00000000000..40bbd2d1a29 --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/pages/third.html @@ -0,0 +1,10 @@ + + + + + Title + + +

Third page

+ + diff --git a/test/functional/fixtures/run-options/disable-page-caching/test.js b/test/functional/fixtures/run-options/disable-page-caching/test.js new file mode 100644 index 00000000000..cf811c08263 --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/test.js @@ -0,0 +1,19 @@ +const { skipDescribeInNativeAutomation } = require('../../../utils/skip-in'); + +// NOTE: This test suit tests localStorage synchronisation between windows. We didn't support multiple windows in CDP yet. +// We need to turn on these tests and implement cache-control response header in nativeAutomation once multiple windows are supported. + +skipDescribeInNativeAutomation('Disable page caching', () => { + it('Test run', () => { + return runTests('testcafe-fixtures/test-run.js', null, { disablePageCaching: true, disableMultipleWindows: true }); + }); + + it('Fixture', () => { + return runTests('testcafe-fixtures/fixture.js', null, { disableMultipleWindows: true }); + }); + + it('Single test', () => { + return runTests('testcafe-fixtures/single-test.js', null, { disableMultipleWindows: true }); + }); +}); + diff --git a/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/fixture.js b/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/fixture.js new file mode 100644 index 00000000000..1506c9583f5 --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/fixture.js @@ -0,0 +1,12 @@ +import { role, url, expectedRoleLastPageLocation } from '../common/index.js'; + +fixture + .disablePageCaching `Fixture` + .page(url) + .beforeEach( async t => { + await t.useRole(role); + }); + +test('test', async t => { + await t.expect(role.lastPageLocation).eql(expectedRoleLastPageLocation); +}); diff --git a/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/single-test.js b/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/single-test.js new file mode 100644 index 00000000000..aa5693c3f9d --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/single-test.js @@ -0,0 +1,14 @@ +import { role, url, expectedRoleLastPageLocation } from '../common/index.js'; + +fixture `Fixture`; + +test + .disablePageCaching + .page(url) + .before(async t => { + await t.useRole(role); + }) + ('test', async t => { + await t.expect(role.lastPageLocation).eql(expectedRoleLastPageLocation); + }); + diff --git a/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/test-run.js b/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/test-run.js new file mode 100644 index 00000000000..d72217d4c75 --- /dev/null +++ b/test/functional/fixtures/run-options/disable-page-caching/testcafe-fixtures/test-run.js @@ -0,0 +1,11 @@ +import { role, url, expectedRoleLastPageLocation } from '../common/index.js'; + +fixture `Fixture` + .page(url) + .beforeEach( async t => { + await t.useRole(role); + }); + +test('test', async t => { + await t.expect(role.lastPageLocation).eql(expectedRoleLastPageLocation); +}); diff --git a/test/functional/fixtures/run-options/request-timeout/pages/index.html b/test/functional/fixtures/run-options/request-timeout/pages/index.html new file mode 100644 index 00000000000..39551e09038 --- /dev/null +++ b/test/functional/fixtures/run-options/request-timeout/pages/index.html @@ -0,0 +1,27 @@ + + + + + Index page + + + Redirect to a page + +
+ Not send +
+ + + diff --git a/test/functional/fixtures/run-options/request-timeout/pages/page.html b/test/functional/fixtures/run-options/request-timeout/pages/page.html new file mode 100644 index 00000000000..5b8b083b22a --- /dev/null +++ b/test/functional/fixtures/run-options/request-timeout/pages/page.html @@ -0,0 +1,10 @@ + + + + + Title + + +

Page

+ + diff --git a/test/functional/fixtures/run-options/request-timeout/test.js b/test/functional/fixtures/run-options/request-timeout/test.js new file mode 100644 index 00000000000..49a2b5298a9 --- /dev/null +++ b/test/functional/fixtures/run-options/request-timeout/test.js @@ -0,0 +1,45 @@ +const { expect } = require('chai'); +const { skipDescribeInNativeAutomation } = require('../../../utils/skip-in'); + +// NOTE: CDP doesn't support request timeouts out of the box. +// We have decided not to implement such functionality until it is supported in CDP. +skipDescribeInNativeAutomation('Request timeout', () => { + describe('Test level', () => { + it('Page request timeout', () => { + return runTests('testcafe-fixtures/test-level.js', 'page request timeout', { only: 'chrome', shouldFail: true }) + .catch(errs => { + expect(errs[0]).contain('Failed to complete a request to "http://localhost:3000/fixtures/run-options/request-timeout/pages/page.html?delay=5000" within the timeout period.'); + }); + }); + + it('Ajax request timeout', () => { + return runTests('testcafe-fixtures/test-level.js', 'ajax request timeout', { only: 'chrome' }); + }); + }); + + describe('Run level', () => { + it('Page request timeout', () => { + return runTests('testcafe-fixtures/run-level.js', 'page request timeout', { pageRequestTimeout: 100, only: 'chrome', shouldFail: true }) + .catch(errs => { + expect(errs[0]).contain('Failed to complete a request to "http://localhost:3000/fixtures/run-options/request-timeout/pages/page.html?delay=5000" within the timeout period.'); + }); + }); + + it('Ajax request timeout', () => { + return runTests('testcafe-fixtures/run-level.js', 'ajax request timeout', { ajaxRequestTimeout: 100, only: 'chrome' }); + }); + }); + + describe('Overriding', () => { + it('Page request timeout', () => { + return runTests('testcafe-fixtures/test-level.js', 'page request timeout', { pageRequestTimeout: 100000, only: 'chrome', shouldFail: true }) + .catch(errs => { + expect(errs[0]).contain('Failed to complete a request to "http://localhost:3000/fixtures/run-options/request-timeout/pages/page.html?delay=5000" within the timeout period.'); + }); + }); + + it('Ajax request timeout', () => { + return runTests('testcafe-fixtures/test-level.js', 'ajax request timeout', { ajaxRequestTimeout: 100000, only: 'chrome' }); + }); + }); +}); diff --git a/test/functional/fixtures/run-options/request-timeout/testcafe-fixtures/run-level.js b/test/functional/fixtures/run-options/request-timeout/testcafe-fixtures/run-level.js new file mode 100644 index 00000000000..f09eff5ef88 --- /dev/null +++ b/test/functional/fixtures/run-options/request-timeout/testcafe-fixtures/run-level.js @@ -0,0 +1,14 @@ +import { Selector } from 'testcafe'; + +fixture ('request timeout') + .page('http://localhost:3000/fixtures/run-options/request-timeout/pages/index.html'); + +test('page request timeout', async t => { + await t.click('#redirect-to-page'); +}); + +test('ajax request timeout', async t => { + await t + .click('#send-xhr') + .expect(Selector('#send-xhr-status').textContent).notEql('Done'); +}); diff --git a/test/functional/fixtures/run-options/request-timeout/testcafe-fixtures/test-level.js b/test/functional/fixtures/run-options/request-timeout/testcafe-fixtures/test-level.js new file mode 100644 index 00000000000..bc55981f546 --- /dev/null +++ b/test/functional/fixtures/run-options/request-timeout/testcafe-fixtures/test-level.js @@ -0,0 +1,18 @@ +import { Selector } from 'testcafe'; + +fixture ('request timeout') + .page('http://localhost:3000/fixtures/run-options/request-timeout/pages/index.html'); + +test + .timeouts({ pageRequestTimeout: 100 }) + ('page request timeout', async t => { + await t.click('#redirect-to-page'); + }); + +test + .timeouts({ ajaxRequestTimeout: 100 }) + ('ajax request timeout', async t => { + await t + .click('#send-xhr') + .expect(Selector('#send-xhr-status').textContent).notEql('Done'); + }); diff --git a/test/functional/fixtures/run-options/selector-timeout/test.js b/test/functional/fixtures/run-options/selector-timeout/test.js index 0ef4b74b6ff..f352eb4ed06 100644 --- a/test/functional/fixtures/run-options/selector-timeout/test.js +++ b/test/functional/fixtures/run-options/selector-timeout/test.js @@ -1,4 +1,4 @@ -var expect = require('chai').expect; +const expect = require('chai').expect; describe('Selector timeout', function () { it('Should pass if selector timeout exceeds time required for the element to appear', function () { @@ -10,7 +10,7 @@ describe('Selector timeout', function () { return runTests('testcafe-fixtures/selector-timeout.js', 'Wait for element with insufficient timeout', { shouldFail: true, selectorTimeout: 500 }) .catch(function (errs) { - expect(errs[0]).to.contains('The element that matches the specified selector is not visible.'); + expect(errs[0]).to.match(/The action target \(.*\) is invisible. The value of its 'display' property is 'none'./); }); }); }); diff --git a/test/functional/fixtures/run-options/speed/test.js b/test/functional/fixtures/run-options/speed/test.js index ae57acb0487..a9fae59c665 100644 --- a/test/functional/fixtures/run-options/speed/test.js +++ b/test/functional/fixtures/run-options/speed/test.js @@ -11,7 +11,7 @@ describe('[API] Speed', function () { return runTests('./testcafe-fixtures/speed-test.js', 'Decrease speed in iframe', { only: 'chrome', speed: 0.4, - selectorTimeout: 10000 + selectorTimeout: 10000, }); }); }); diff --git a/test/functional/fixtures/run-options/speed/testcafe-fixtures/speed-test.js b/test/functional/fixtures/run-options/speed/testcafe-fixtures/speed-test.js index 6a6f88504a0..d0f855279b3 100644 --- a/test/functional/fixtures/run-options/speed/testcafe-fixtures/speed-test.js +++ b/test/functional/fixtures/run-options/speed/testcafe-fixtures/speed-test.js @@ -7,7 +7,7 @@ test('Decrease speed', async t => { // NOTE: do the first click to wait while the page is loaded await t.click('#button1'); - var startTime = Date.now(); + const startTime = Date.now(); await t.click('#button1'); @@ -20,7 +20,7 @@ test('Decrease speed in iframe', async t => { .switchToIframe('#iframe') .click('#button1'); - var startTime = Date.now(); + const startTime = Date.now(); await t.click('#button1'); @@ -31,7 +31,7 @@ test('Default speed', async t => { // NOTE: do the first click to wait while the page is loaded await t.click('#button1'); - var startTime = Date.now(); + const startTime = Date.now(); await t.click('#button1'); diff --git a/test/functional/fixtures/run-options/stop-on-first-fail/test.js b/test/functional/fixtures/run-options/stop-on-first-fail/test.js new file mode 100644 index 00000000000..bf40a79ef1a --- /dev/null +++ b/test/functional/fixtures/run-options/stop-on-first-fail/test.js @@ -0,0 +1,55 @@ +const expect = require('chai').expect; +const fs = require('fs'); +const { createSimpleTestStream } = require('../../../utils/stream'); +const ReporterPluginHost = require('../../../../../lib/reporter/plugin-host'); + +const TEST_RUN_COUNT_FILENAME = 'testRunCount.txt'; + +const getTestRunCount = () => { + const content = fs.readFileSync(TEST_RUN_COUNT_FILENAME).toString(); + + return parseInt(content, 10); +}; + +describe('Stop test task on first failed test', () => { + afterEach(() => { + // NOTE: after mocha is updated to `^7.1.1` the `afterEach` hook is called if the test is skipped/pending + // before the update the `afterEach` hook was not called. + // When we run the test not in `chrome` the file will not exist, since we have the { only: 'chrome' } option, + // so we cannot unlink it. + if (fs.existsSync(TEST_RUN_COUNT_FILENAME)) + fs.unlinkSync(TEST_RUN_COUNT_FILENAME); + }); + + it('Basic', () => { + return runTests('./testcafe-fixtures/stop-on-first-fail-test.js', void 0, { + shouldFail: true, + stopOnFirstFail: true, + only: 'chrome', + }).catch(() => { + expect(getTestRunCount()).eql(2); + expect(testReport.failedCount).eql(1); + }); + }); + + it('Reporting', () => { + const stream = createSimpleTestStream(); + + return runTests('./testcafe-fixtures/stop-on-first-fail-test.js', void 0, { + shouldFail: true, + stopOnFirstFail: true, + reporter: [{ + name: 'spec', + output: stream, + }], + }).catch(() => { + const pluginHost = new ReporterPluginHost({ noColors: true }); + const { ok, err } = pluginHost.symbols; + + expect(stream.data).contains(`${ok} test1`); + expect(stream.data).contains(`${err} test2`); + expect(stream.data).to.not.contains(`${ok} test3`); + expect(stream.data).contains('2/3 failed'); + }); + }); +}); diff --git a/test/functional/fixtures/run-options/stop-on-first-fail/testcafe-fixtures/stop-on-first-fail-test.js b/test/functional/fixtures/run-options/stop-on-first-fail/testcafe-fixtures/stop-on-first-fail-test.js new file mode 100644 index 00000000000..ab03d478ee5 --- /dev/null +++ b/test/functional/fixtures/run-options/stop-on-first-fail/testcafe-fixtures/stop-on-first-fail-test.js @@ -0,0 +1,25 @@ +import fs from 'fs'; + +fixture `Stop on first fail test`; + +let testRunCount = 0; + +const updateTestRunCount = () => { + testRunCount++; + + fs.writeFileSync('testRunCount.txt', testRunCount.toString()); +}; + +test('test1', async () => { + updateTestRunCount(); +}); + +test('test2', async t => { + updateTestRunCount(); + + await t.expect(false).ok(); +}); + +test('test3', async () => { + updateTestRunCount(); +}); diff --git a/test/functional/fixtures/runner/test.js b/test/functional/fixtures/runner/test.js new file mode 100644 index 00000000000..e50a759350d --- /dev/null +++ b/test/functional/fixtures/runner/test.js @@ -0,0 +1,46 @@ +const path = require('path'); +const osFamily = require('os-family'); +const { expect } = require('chai'); +const config = require('../../config'); + +function run (browsers, testFile) { + return testCafe + .createRunner() + .src(path.join(__dirname, testFile)) + .browsers(browsers) + .run({ disableNativeAutomation: !config.nativeAutomation }); +} + +describe('Runner', () => { + if (config.useLocalBrowsers && !config.useHeadlessBrowsers && osFamily.linux) { + let originalDisplay = null; + + before(() => { + originalDisplay = process.env.DISPLAY; + + process.env.DISPLAY = ''; + }); + + after(() => { + process.env.DISPLAY = originalDisplay; + }); + + it('Should throw an error when tests are run on Linux without graphical subsystem', async () => { + try { + await run('chromium', './testcafe-fixtures/basic-test.js'); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql( + `Your Linux version does not have a graphic subsystem to run chromium with a GUI. ` + + `You can launch the browser in headless mode. ` + + `If you use a portable browser version, ` + + `specify the browser alias before the path instead of the 'path' prefix. ` + + `For more information, see ` + + `https://devexpress.github.io/testcafe/documentation/guides/concepts/browsers.html#test-in-headless-mode` + ); + } + }); + } +}); diff --git a/test/functional/fixtures/runner/testcafe-fixtures/basic-test.js b/test/functional/fixtures/runner/testcafe-fixtures/basic-test.js new file mode 100644 index 00000000000..ca9ee698d6b --- /dev/null +++ b/test/functional/fixtures/runner/testcafe-fixtures/basic-test.js @@ -0,0 +1,5 @@ +fixture `Runner`; + +test(`Basic test`, async t => { + await t.expect(true).ok(); +}); diff --git a/test/functional/fixtures/screenshots-on-fails/pages/crop.html b/test/functional/fixtures/screenshots-on-fails/pages/crop.html new file mode 100644 index 00000000000..084ffd74c99 --- /dev/null +++ b/test/functional/fixtures/screenshots-on-fails/pages/crop.html @@ -0,0 +1,36 @@ + + + + + + + + Crop screenshots + + + +
+
+
+ + diff --git a/test/functional/fixtures/screenshots-on-fails/pages/index.html b/test/functional/fixtures/screenshots-on-fails/pages/index.html index 30bac9fb981..3985f155dfa 100644 --- a/test/functional/fixtures/screenshots-on-fails/pages/index.html +++ b/test/functional/fixtures/screenshots-on-fails/pages/index.html @@ -6,7 +6,7 @@ + + \ No newline at end of file diff --git a/test/functional/fixtures/ui/test.js b/test/functional/fixtures/ui/test.js new file mode 100644 index 00000000000..f398b477d98 --- /dev/null +++ b/test/functional/fixtures/ui/test.js @@ -0,0 +1,51 @@ +describe('TestCafe UI', () => { + describe('Status Bar', () => { + it('Should display correct status', () => { + return runTests('./testcafe-fixtures/status-bar-test.js', 'Show status prefix', { assertionTimeout: 3000 }); + }); + + it('Hide elements when resizing the window', () => { + return runTests('./testcafe-fixtures/status-bar-test.js', 'Hide elements when resizing the window', { skip: ['android', 'ipad', 'iphone', 'edge', 'safari'] }); + }); + + it('Should hide the status bar even if document was hidden during initialization (GH-7384)', function () { + return runTests('./testcafe-fixtures/status-bar-test.js', 'Hide status bar after mouse move', { only: ['chrome'] }); + }); + }); + + describe('Selector Inspector', () => { + function runTestCafeTest (testName) { + it (testName, function () { + return runTests('./testcafe-fixtures/selector-inspector-test.js', testName, { skip: ['android', 'ipad', 'iphone'] }); + }); + } + + runTestCafeTest('panel should be shown in debug mode'); + + runTestCafeTest('should hide TestCafe elements while piking'); + + runTestCafeTest('should generate valid selector'); + + runTestCafeTest('should fill the selectors list with the generated selectors'); + + runTestCafeTest('should indicate the correct number of elements matching the css selector'); + + runTestCafeTest('should indicate the correct number of elements matching the TestCafe selector'); + + runTestCafeTest('should indicate if the selector is invalid on input'); + + runTestCafeTest('should indicate that no matches on input'); + + runTestCafeTest('should highlight matches elements on input'); + + runTestCafeTest('should place a selector selected from the list in the input field'); + + runTestCafeTest('should copy selector'); + + runTestCafeTest('should hide panel'); + + runTestCafeTest('should indicate the correct number of elements matching the TestCafe selector passed in debug'); + + runTestCafeTest('should indicate single element matching the TestCafe selector passed in debug'); + }); +}); diff --git a/test/functional/fixtures/ui/testcafe-fixtures/selector-inspector-test.js b/test/functional/fixtures/ui/testcafe-fixtures/selector-inspector-test.js new file mode 100644 index 00000000000..11386736fe7 --- /dev/null +++ b/test/functional/fixtures/ui/testcafe-fixtures/selector-inspector-test.js @@ -0,0 +1,313 @@ +import { ClientFunction } from 'testcafe'; + +fixture `Selector Inspector` + .clientScripts `../utils/selector-inspector.js` + .page `http://localhost:3000/fixtures/ui/pages/example.html`; + +test('panel should be shown in debug mode', async t => { + await ClientFunction(() => { + window['%testCafeDriverInstance%'].selectorInspectorPanel.show = () => window.resumeTest(); + })(); + + await t.debug(); +}); + +test('should hide TestCafe elements while piking', async t => { + await ClientFunction(() => { + const { + startPicking, + getShadowUIElements, + canBeShownInPicking, + isVisible, + resumeTest, + } = window; + + startPicking().then(() => { + for (const child of getShadowUIElements()) { + if (!canBeShownInPicking(child) && isVisible(child)) + return; + } + + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should generate valid selector', async t => { + await ClientFunction(() => { + const { + startPicking, + pickElement, + getSelectorInputValue, + resumeTest, + } = window; + + startPicking() + .then(() => { + const target = document.querySelector('#container > div:nth-child(2)'); + + pickElement(target); + + return getSelectorInputValue(); + }) + .then(value => { + if (value === "Selector('#container div').withText('Another text')") + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should fill the selectors list with the generated selectors', async t => { + await ClientFunction(() => { + const { + startPicking, + pickElement, + expandSelectorsList, + getSelectorsListValues, + resumeTest, + } = window; + + startPicking() + .then(() => { + const target = document.querySelector('div'); + + pickElement(target); + + return expandSelectorsList(); + }) + .then(() => { + return getSelectorsListValues(); + }) + .then(selectorsValues => { + const isValidTestCafeSelectors = selectorsValues.every(value => value.startsWith('Selector(')); + + if (selectorsValues.length > 0 && isValidTestCafeSelectors) + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should indicate the correct number of elements matching the css selector', async t => { + await ClientFunction(() => { + const { typeSelector, getMatchIndicatorInnerText, resumeTest } = window; + + typeSelector('div') + .then(() => { + return getMatchIndicatorInnerText(); + }) + .then(text => { + if (text === 'Found: 4') + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should indicate the correct number of elements matching the TestCafe selector', async t => { + await ClientFunction(() => { + const { typeSelector, getMatchIndicatorInnerText, resumeTest } = window; + + typeSelector("Selector('div')") + .then(() => { + return getMatchIndicatorInnerText(); + }) + .then(text => { + if (text === 'Found: 4') + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should indicate if the selector is invalid on input', async t => { + await ClientFunction(() => { + const { typeSelector, getMatchIndicatorInnerText, resumeTest } = window; + + typeSelector('Selector(/.%') + .then(() => { + return getMatchIndicatorInnerText(); + }) + .then(text => { + if (text === 'Invalid Selector') + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should indicate that no matches on input', async t => { + await ClientFunction(() => { + const { typeSelector, getMatchIndicatorInnerText, resumeTest } = window; + + typeSelector('div > div > input[id="not-valid-id"]') + .then(() => { + return getMatchIndicatorInnerText(); + }) + .then(text => { + if (text === 'No Matching Elements') + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should highlight matches elements on input', async t => { + await ClientFunction(() => { + const { + typeSelector, + getElementFrames, + isElementsRectsEql, + resumeTest, + } = window; + + const selector = 'input[id]'; + const elements = document.querySelectorAll(selector); + + if (!elements || elements.length !== 4) + return; + + typeSelector(selector) + .then(() => { + return getElementFrames(); + }) + .then(elementFrames => { + for (let i = 0; i < elements.length; i++) { + if (!isElementsRectsEql(elements[i], elementFrames[i])) + return; + } + + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should place a selector selected from the list in the input field', async t => { + await ClientFunction(() => { + const { + startPicking, + pickElement, + getGeneratedSelectors, + selectSelectorFromList, + getSelectorInputValue, + resumeTest, + } = window; + + startPicking() + .then(() => { + const target = document.querySelector('#main > p:nth-child(3)'); + + pickElement(target); + + const selectors = getGeneratedSelectors(); + + let promise = Promise.resolve(); + + for (let i = 0; i < selectors.length; i++) { + promise = promise + .then(() => { + return selectSelectorFromList(i); + }) + .then(() => { + return getSelectorInputValue(); + }) + .then(selectorValue => { + if (selectorValue !== selectors[i]) + throw 'Selector values do not match'; + }); + } + + return promise; + }) + .then(() => { + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should copy selector', async t => { + await ClientFunction(() => { + const { typeSelector, copySelector, resumeTest } = window; + + const selector = "Selector('div').nth(2).find('input')"; + + typeSelector(selector) + .then(() => { + return copySelector(); + }) + .then(copiedSelector => { + if (copiedSelector === selector) + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should hide panel', async t => { + await ClientFunction(() => { + const { + getHideButtonElements, + resumeTest, + click, + getSelectorPanelContainer, + getComputedStyle, + } = window; + + getHideButtonElements() + .then(({ hideButton, hideButtonSpan }) => { + if (getComputedStyle(hideButtonSpan, '::after').content === '"Hide Picker"') + click(hideButton); + if (getComputedStyle(hideButtonSpan, '::after').content === '"Show Picker"') + return getSelectorPanelContainer(); + return Promise.resolve(); + }) + .then(selectorPanelContainer => { + if (getComputedStyle(selectorPanelContainer).display === 'none') + resumeTest(); + }); + })(); + + await t.debug(); +}); + +test('should indicate the correct number of elements matching the TestCafe selector passed in debug', async t => { + await ClientFunction(() => { + const { getMatchIndicatorInnerText, resumeTest } = window; + + getMatchIndicatorInnerText() + .then(text => { + if (text === 'Found: 5') + resumeTest(); + }); + })(); + + await t.debug('p'); +}); + +test('should indicate single element matching the TestCafe selector passed in debug', async t => { + await ClientFunction(() => { + const { getMatchIndicatorInnerText, resumeTest } = window; + + getMatchIndicatorInnerText() + .then(text => { + if (text === 'Found: 1') + resumeTest(); + }); + })(); + + await t.debug('#container'); +}); diff --git a/test/functional/fixtures/ui/testcafe-fixtures/status-bar-test.js b/test/functional/fixtures/ui/testcafe-fixtures/status-bar-test.js new file mode 100644 index 00000000000..ca73ac14616 --- /dev/null +++ b/test/functional/fixtures/ui/testcafe-fixtures/status-bar-test.js @@ -0,0 +1,158 @@ +import { Selector, ClientFunction } from 'testcafe'; +import assert from 'assert'; +import osFamily from 'os-family'; +import { saveWindowState, restoreWindowState } from '../../../esm-utils/window-helpers.js'; + +fixture `Status Bar` + .page `http://localhost:3000/fixtures/ui/pages/empty-page.html` + .beforeEach(async t => { + await saveWindowState(t); + }) + .afterEach(async t => { + await restoreWindowState(t); + }); + +test('Show status prefix', async t => { + const statusDiv = Selector(() => window['%testCafeDriverInstance%'].statusBar.statusDiv); + const progressBar = Selector(() => window['%testCafeDriverInstance%'].statusBar.progressBar.progressBar); + const setStatusPrefix = ClientFunction(statusPrefix => { + const statusBar = window['%testCafeDriverInstance%'].statusBar; + + statusBar.progressBar.show(); + statusBar.setStatusPrefix(statusPrefix); + }); + + let statusText = await statusDiv.innerText; + + await t + .expect(statusText).notOk() + .expect(statusDiv.innerText).eql('Waiting for assertion execution...'); + + await setStatusPrefix('Status prefix'); + + const progressBarVisible = await progressBar.visible; + + statusText = await statusDiv.innerText; + + await t + .expect(statusText.trim()).eql('Status prefix') + .expect(progressBarVisible).ok() + .expect(statusDiv.innerText).eql('Status prefix. Waiting for assertion execution...') + .navigateTo('about:blank') + .expect(statusDiv.innerText).eql('Status prefix. Waiting for assertion execution...'); + + await setStatusPrefix('Modified status prefix'); + + statusText = await statusDiv.innerText; + + await t + .expect(statusText.trim()).eql('Modified status prefix'); +}); + +test('Hide elements when resizing the window', async t => { + const statusBarDiv = Selector(() => window['%testCafeDriverInstance%'].statusBar.statusBar); + const statusDiv = Selector(() => window['%testCafeDriverInstance%'].statusBar.statusDiv); + const icon = Selector(() => window['%testCafeDriverInstance%'].statusBar.icon); + const buttons = Selector(() => window['%testCafeDriverInstance%'].statusBar.buttons); + const userAgent = statusBarDiv.find('.user-agent-hammerhead-shadow-ui'); + + await t + .eval(() => { + const statusBar = window['%testCafeDriverInstance%'].statusBar; + + statusBar.setStatusPrefix('Status prefix'); + statusBar.showDebuggingStatus(); + }); + + await t + .resizeWindow(1000, 400); + + //If we await these properties during the assertion execution, the status will be changed to "Waiting for..." + const getStatusBarItemsVisibility = async () => { + const userAgentVisible = await userAgent.visible; + const statusVisible = await statusDiv.visible; + const buttonCaptionsVisible = await buttons.find('span').filterVisible().count === 3; + const iconVisible = await icon.visible; + + return { userAgentVisible, statusVisible, buttonCaptionsVisible, iconVisible }; + }; + + let itemsVisibility = await getStatusBarItemsVisibility(); + + if (!osFamily.mac) { + await t + .expect(itemsVisibility.userAgentVisible).ok() + .expect(itemsVisibility.statusVisible).ok() + .expect(itemsVisibility.buttonCaptionsVisible).ok() + .expect(itemsVisibility.iconVisible).ok(); + } + + await t.resizeWindow(800, 400); + + itemsVisibility = await getStatusBarItemsVisibility(); + + await t + .expect(itemsVisibility.userAgentVisible).notOk() + .expect(itemsVisibility.statusVisible).ok() + .expect(itemsVisibility.buttonCaptionsVisible).ok() + .expect(itemsVisibility.iconVisible).ok() + .resizeWindow(600, 400); + + itemsVisibility = await getStatusBarItemsVisibility(); + + await t + .expect(itemsVisibility.userAgentVisible).notOk() + .expect(itemsVisibility.statusVisible).ok() + .expect(itemsVisibility.buttonCaptionsVisible).notOk() + .expect(itemsVisibility.iconVisible).notOk() + .resizeWindow(520, 400); + + itemsVisibility = await getStatusBarItemsVisibility(); + + await t + .expect(itemsVisibility.userAgentVisible).notOk() + .expect(itemsVisibility.statusVisible).ok() + .expect(itemsVisibility.buttonCaptionsVisible).notOk() + .expect(itemsVisibility.iconVisible).notOk() + .expect(statusBarDiv.clientHeight).eql(82); +}); + +const TOTAL_DELAY = 5000; +const START_DELAY = 1000; +const AFTER_DELAY = 2000; + +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + +async function evaluateOnRemote (client, func) { + const { result } = await client.Runtime.evaluate({ expression: `(${func.toString()})()`, returnByValue: true }); + + return result.value; +} + +async function simulateMoveAndCheckStatusBar (t) { + await delay(START_DELAY); + + const browserConnection = t.testRun.browserConnection; + const providerPlugin = browserConnection.provider.plugin; + const browserClient = providerPlugin._getBrowserProtocolClient(providerPlugin.openedBrowsers[browserConnection.id]); + const remoteInterface = await browserClient.getActiveClient(); + + const windowSize = await evaluateOnRemote(remoteInterface, () => ({ width: innerWidth, height: innerHeight })); + + await remoteInterface.Input.dispatchMouseEvent({ type: 'mouseMoved', x: windowSize.width / 2, y: windowSize.height - 1 }); + + await delay(AFTER_DELAY); + + const statusBarState = await evaluateOnRemote(remoteInterface, () => window['%testCafeDriverInstance%'].statusBar.state); + + assert.ok(statusBarState.hidden); +} + +test + .page('../pages/hiding.html') + ('Hide status bar after mouse move', async t => { + await Promise.all([ + t.wait(TOTAL_DELAY), + simulateMoveAndCheckStatusBar(t), + ]); + }); diff --git a/test/functional/fixtures/ui/utils/selector-inspector.js b/test/functional/fixtures/ui/utils/selector-inspector.js new file mode 100644 index 00000000000..6079085b894 --- /dev/null +++ b/test/functional/fixtures/ui/utils/selector-inspector.js @@ -0,0 +1,234 @@ +Object.assign(window, { + isVisible (element) { + return element.style.visibility !== 'hidden' && element.style.display !== 'none'; + }, + + canBeShownInPicking (element) { + return [ + 'element-frame-hammerhead-shadow-ui', + 'tooltip-hammerhead-shadow-ui', + 'arrow-hammerhead-shadow-ui', + ].includes(element.className); + }, + + isElementsRectsEql (firstElement, secondElement) { + const firstRect = firstElement.getBoundingClientRect(); + const secondRect = secondElement.getBoundingClientRect(); + + for (const key in firstElement) { + if (firstRect[key] !== secondRect[key]) + return false; + } + + return true; + }, + + getSelectedValue () { + const { nativeMethods } = window['%hammerhead%']; + + const activeElement = nativeMethods.documentActiveElementGetter.call(document); + + return activeElement.value.substring(activeElement.selectionStart, activeElement.selectionend); + }, + + simulateEvent (element, eventName, options) { + const { eventSandbox } = window['%hammerhead%']; + + eventSandbox.eventSimulator[eventName](element, options); + }, + + click (element) { + this.simulateEvent(element, 'click'); + }, + + mousedown (element) { + this.simulateEvent(element, 'mousedown'); + }, + + mousemove (element) { + const { x, y } = element.getBoundingClientRect(); + + this.simulateEvent(element, 'mousemove', { clientX: x + 1, clientY: y + 1 }); + }, + + input (element, value) { + this.simulateEvent(element, 'input', value); + }, + + focus (element) { + this.simulateEvent(element, 'focus'); + }, + + getShadowUIElements () { + return window['%hammerhead%'].shadowUI.root.firstChild.children; + }, + + pickElement (element) { + this.mousemove(element); + this.click(element); + }, + + getGeneratedSelectors () { + const { elementPicker } = window['%testCafeDriverInstance%'].selectorInspectorPanel; + + return elementPicker.actualSelectors.map(selector => selector.value); + }, + + querySelector (cssSelector, element = document) { + const { nativeMethods } = window['%hammerhead%']; + + return nativeMethods.querySelector.call(element, cssSelector); + }, + + querySelectorAll (cssSelector, element = document) { + const { nativeMethods } = window['%hammerhead%']; + + return nativeMethods.querySelectorAll.call(element, cssSelector); + }, + + async retryExecute (fn, retryTimeout = 80) { + return new Promise(resolve => { + const intervalId = setInterval(() => fn(result => { + clearInterval(intervalId); + resolve(result); + }), retryTimeout); + }); + }, + + async getElement (cssSelector) { + return this.retryExecute(resolve => { + const element = this.querySelector(cssSelector); + + if (element) + resolve(element); + }); + }, + + async getElements (cssSelector) { + return this.retryExecute(resolve => { + const elements = this.querySelectorAll(cssSelector); + + if (elements && elements.length) + resolve(elements); + }); + }, + + async resumeTest () { + const resumeButton = await this.getElement('.resume-hammerhead-shadow-ui'); + + this.mousedown(resumeButton); + }, + + async startPicking () { + const pickButton = await this.getElement('.pick-button-hammerhead-shadow-ui'); + + this.click(pickButton); + }, + + async getSelectorInput () { + return this.getElement('.selector-input-hammerhead-shadow-ui'); + }, + + async getSelectorInputValue () { + return this.getSelectorInput().then(input => input.value); + }, + + async typeSelector (value) { + const selectorInput = await this.getSelectorInput(); + + selectorInput.value = value; + + selectorInput.focus(); + }, + + async getMatchIndicator () { + return this.getElement('.match-indicator-hammerhead-shadow-ui'); + }, + + async getMatchIndicatorInnerText () { + return this.getMatchIndicator().then(indicator => indicator.innerText); + }, + + async expandSelectorsList () { + const expandButton = await this.getElement('.expand-selector-list-hammerhead-shadow-ui'); + + this.click(expandButton); + }, + + async getSelectorsList () { + return this.getElement('.selectors-list-hammerhead-shadow-ui'); + }, + + async getSelectorsListValues () { + const selectorsList = await this.getSelectorsList(); + const values = []; + + for (const selectorValueElement of selectorsList.children) + values.push(selectorValueElement.innerText); + + return values; + }, + + async selectSelectorFromList (index) { + await this.expandSelectorsList(); + + const selectorsList = await this.getSelectorsList(); + const selectorValueElement = selectorsList.children[index]; + + this.click(selectorValueElement); + }, + + async getElementFrames () { + const RENDERING_DELAY = 200; + const CSS_SELECTOR = '.element-frame-hammerhead-shadow-ui'; + + await this.getElement(CSS_SELECTOR); + + await new Promise(resolve => setTimeout(resolve, RENDERING_DELAY)); + + return this.querySelectorAll(CSS_SELECTOR); + }, + + async mockOnceCopyCommand () { + const originExecCommand = document.execCommand; + + return new Promise(resolve => { + document.execCommand = cmd => { + if (cmd !== 'copy') + return originExecCommand.call(document, cmd); + + document.execCommand = originExecCommand; + + resolve(this.getSelectedValue()); + + return document.execCommand(cmd); + }; + }); + }, + + async copySelector () { + const copyButton = await this.getElement('input[value="Copy"]'); + + const promise = this.mockOnceCopyCommand(); + + this.click(copyButton); + + return promise; + }, + + async getHideButtonElements () { + const hideButton = await this.getElement('.selector-panel-toggle-button-hammerhead-shadow-ui'); + const hideButtonSpan = await this.getElement('.selector-panel-toggle-button-container-hammerhead-shadow-ui span'); + + return { + hideButton, + hideButtonSpan, + }; + }, + + async getSelectorPanelContainer () { + const selectorPanelContainer = await this.getElement('.selector-inspector-panel-container-hammerhead-shadow-ui'); + + return selectorPanelContainer; + }, +}); diff --git a/test/functional/fixtures/video-recording/pages/index.html b/test/functional/fixtures/video-recording/pages/index.html new file mode 100644 index 00000000000..92df70d90c1 --- /dev/null +++ b/test/functional/fixtures/video-recording/pages/index.html @@ -0,0 +1,8 @@ + + + Video + + +

Video recording test

+ + diff --git a/test/functional/fixtures/video-recording/skip/ffmpeg-test.js b/test/functional/fixtures/video-recording/skip/ffmpeg-test.js new file mode 100644 index 00000000000..10c8b3632e0 --- /dev/null +++ b/test/functional/fixtures/video-recording/skip/ffmpeg-test.js @@ -0,0 +1,39 @@ +const createTestCafe = require('../../../../../'); +const sinon = require('sinon'); +const assert = require('assert'); +const config = require('../../../config'); +const VideoRecorder = require('../../../../../lib/video-recorder/process'); + +if (config.useLocalBrowsers) { + describe('FFMPEG shouldn\'t run, when fixture skipped', () => { + it('Start fixture with skip and look for FFMPEG init function', async function () { + this.timeout(30000); + + let ffmpegWasCalled = false; + + sinon.stub(VideoRecorder.prototype, 'init').callsFake(() => { + ffmpegWasCalled = true; + }); + + let runner; + let testcafe; + + await createTestCafe('localhost', '', '') + .then(testcafeInstance => { + runner = testcafeInstance.createRunner(); + testcafe = testcafeInstance; + }) + .then(() => { + return runner + .src('test/functional/fixtures/video-recording/skip/fixture.test.js') + .browsers('chrome') + .video('reports') + .run({ disableNativeAutomation: !config.nativeAutomation }); + }) + .then(async () => { + testcafe.close(); + assert.deepStrictEqual(ffmpegWasCalled, false); + }); + }); + }); +} diff --git a/test/functional/fixtures/video-recording/skip/fixture.test.js b/test/functional/fixtures/video-recording/skip/fixture.test.js new file mode 100644 index 00000000000..dc070aa1237 --- /dev/null +++ b/test/functional/fixtures/video-recording/skip/fixture.test.js @@ -0,0 +1,6 @@ +fixture.skip `Skip` + .page `http://devexpress.github.io/testcafe/example`; + +test('Test', () => { + // Starts at http://devexpress.github.io/testcafe/example +}); diff --git a/test/functional/fixtures/video-recording/test.js b/test/functional/fixtures/video-recording/test.js new file mode 100644 index 00000000000..f5159056ac2 --- /dev/null +++ b/test/functional/fixtures/video-recording/test.js @@ -0,0 +1,230 @@ +const { expect } = require('chai'); +const path = require('path'); +const { uniq } = require('lodash'); +const config = require('../../config'); +const assertionHelper = require('../../assertion-helper.js'); +const { createReporter } = require('../../utils/reporter'); +const ffprobe = require('@ffprobe-installer/ffprobe'); +const childProcess = require('child_process'); + +function customReporter (errs, videos) { + return createReporter({ + async reportTestDone (name, testRunInfo) { + testRunInfo.errs.forEach(err => { + errs[err.errMsg] = true; + }); + + testRunInfo.videos.forEach(video => { + videos.push(video); + }); + }, + }); +} + +function checkVideoPaths (videoLog, videoPaths) { + const testRunIds = uniq(videoLog.map(video => video.testRunId)); + + expect(videoLog.length).eql(testRunIds.length); + + const loggedPaths = uniq(videoLog.map(video => video.videoPath)).sort(); + const actualPaths = [...videoPaths].sort(); + + expect(loggedPaths.length).eql(videoPaths.length); + + for (let i = 0; i < loggedPaths.length; i++) + expect(path.relative(actualPaths[i], loggedPaths[i])).eql(''); +} + +function getVideoDuration (videoPath) { + const command = `${ffprobe.path} -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 ${videoPath}`; + const result = childProcess.execSync(command); + + return Math.ceil(parseFloat(result.toString())); +} + +const BROWSERS_SUPPORTING_VIDEO_RECORDING = ['chrome', 'firefox']; +const BROWSERS_SUPPORTING_VIDEO_RECORDING_STR = BROWSERS_SUPPORTING_VIDEO_RECORDING.toString(); +const COUNT_AFFECTED_BROWSERS = config.browsers.filter(browser => BROWSERS_SUPPORTING_VIDEO_RECORDING.includes(browser.alias)).length; + +if (config.useLocalBrowsers) { + describe('Video Recording', () => { + afterEach(assertionHelper.removeVideosDir); + + it('Should record video without options', () => { + const errs = {}; + const videos = []; + + return runTests('./testcafe-fixtures/index-test.js', '', { + only: BROWSERS_SUPPORTING_VIDEO_RECORDING_STR, + setVideoPath: true, + reporter: customReporter(errs, videos), + }) + .then(() => { + const errors = Object.keys(errs); + + expect(errors.length).to.equal(2); + expect(errors[0]).to.match(/^Error: Error 1/); + expect(errors[1]).to.match(/^Error: Error 2/); + }) + .then(assertionHelper.getVideoFilesList) + .then(videoFiles => { + expect(videoFiles.length).to.equal(3 * COUNT_AFFECTED_BROWSERS); + + checkVideoPaths(videos, videoFiles); + }); + }); + + it('Should record video in a single file', () => { + const errs = {}; + const videos = []; + + return runTests('./testcafe-fixtures/index-test.js', '', { + only: BROWSERS_SUPPORTING_VIDEO_RECORDING_STR, + shouldFail: true, + setVideoPath: true, + reporter: customReporter(errs, videos), + + videoOptions: { + singleFile: true, + }, + }) + .then(() => { + const errors = Object.keys(errs); + + expect(errors.length).to.equal(2); + expect(errors[0]).to.match(/^Error: Error 1/); + expect(errors[1]).to.match(/^Error: Error 2/); + }) + .then(assertionHelper.getVideoFilesList) + .then(videoFiles => { + expect(videoFiles.length).to.equal(1 * COUNT_AFFECTED_BROWSERS); + + checkVideoPaths(videos, videoFiles); + }); + }); + + it('Should record only failed tests', () => { + const errs = {}; + const videos = []; + + return runTests('./testcafe-fixtures/index-test.js', '', { + only: BROWSERS_SUPPORTING_VIDEO_RECORDING_STR, + shouldFail: true, + setVideoPath: true, + reporter: customReporter(errs, videos), + + videoOptions: { + failedOnly: true, + }, + }) + .then(() => { + const errors = Object.keys(errs); + + expect(errors.length).to.equal(2); + expect(errors[0]).to.match(/^Error: Error 1/); + expect(errors[1]).to.match(/^Error: Error 2/); + }) + .then(assertionHelper.getVideoFilesList) + .then(videoFiles => { + expect(videoFiles.length).to.equal(2 * COUNT_AFFECTED_BROWSERS); + + checkVideoPaths(videos, videoFiles); + }); + }); + + it('Should record only failed tests in a single file', () => { + return runTests('./testcafe-fixtures/index-test.js', '', { + only: BROWSERS_SUPPORTING_VIDEO_RECORDING_STR, + shouldFail: true, + setVideoPath: true, + + videoOptions: { + failedOnly: true, + singleFile: true, + }, + }) + .catch(assertionHelper.getVideoFilesList) + .catch(errors => { + expect(errors.length).to.equal(2); + expect(errors[0]).to.match(/^Error: Error 1/); + expect(errors[1]).to.match(/^Error: Error 2/); + }) + .then(videoFiles => { + expect(videoFiles.length).to.equal(1 * COUNT_AFFECTED_BROWSERS); + }); + }); + + it('Should record video with quarantine mode enabled (multiple attempts)', () => { + const errs = {}; + const videos = []; + + return runTests('./testcafe-fixtures/quarantine-test.js', 'quarantine with attempts', { + only: 'chrome', + quarantineMode: true, + setVideoPath: true, + reporter: customReporter(errs, videos), + }) + .then(assertionHelper.getVideoFilesList) + .then(videoFiles => { + expect(videoFiles.length).eql(1); + expect(videos[0].timecodes.length).eql(4); + expect(videos[0].timecodes[0]).eql(0); + expect(videos[0].timecodes.filter(tc => tc > 0).length).eql(3); + + checkVideoPaths(videos, videoFiles); + }); + }); + + it('Should record video with quarantine mode enabled (no attempts)', () => { + const errs = {}; + const videos = []; + + return runTests('./testcafe-fixtures/quarantine-test.js', 'quarantine without attempts', { + only: 'chrome', + quarantineMode: true, + setVideoPath: true, + reporter: customReporter(errs, videos), + }) + .then(assertionHelper.getVideoFilesList) + .then(videoFiles => { + expect(videoFiles.length).to.equal(1); + + checkVideoPaths(videos, videoFiles); + }); + }); + + it('Should display the warning if there is the not suitable placeholder for the "pathPattern" option was specified', () => { + return runTests('./testcafe-fixtures/index-test.js', '', { + only: 'chrome', + shouldFail: true, + setVideoPath: true, + + videoOptions: { + singleFile: true, + pathPattern: '${TEST_INDEX}_.mp4', + }, + }) + .catch(() => { + expect(testReport.warnings).eql(['TestCafe could not apply the following video recording save path pattern: "${TEST_INDEX}".\n' + + 'You may encounter this behavior when you enable the "singleFile" video recording option and use test-specific path patterns.' + + '\n\n' + + 'The placeholder was replaced with an empty string.']); + }); + }); + + it('Should record a correct video for test with only "wait action"', () => { + return runTests('./testcafe-fixtures/only-wait.js', '', { + only: 'chrome', + setVideoPath: true, + }) + .then(assertionHelper.getVideoFilesList) + .then(videoFiles => { + expect(videoFiles.length).to.equal(1); + + const duration = getVideoDuration(videoFiles[0]); + + expect(duration).gte(10); + }); + }); + }); +} diff --git a/test/functional/fixtures/video-recording/testcafe-fixtures/index-test.js b/test/functional/fixtures/video-recording/testcafe-fixtures/index-test.js new file mode 100644 index 00000000000..aaf076a2a2f --- /dev/null +++ b/test/functional/fixtures/video-recording/testcafe-fixtures/index-test.js @@ -0,0 +1,18 @@ +fixture `Video` + .page `../pages/index.html`; + +test('First', async t => { + await t.wait(2000); +}); + +test('Second', async t => { + await t.wait(2000); + + throw new Error('Error 1'); +}); + +test('Third', async t => { + await t.wait(2000); + + throw new Error('Error 2'); +}); diff --git a/test/functional/fixtures/video-recording/testcafe-fixtures/only-wait.js b/test/functional/fixtures/video-recording/testcafe-fixtures/only-wait.js new file mode 100644 index 00000000000..eeb6841d41e --- /dev/null +++ b/test/functional/fixtures/video-recording/testcafe-fixtures/only-wait.js @@ -0,0 +1,5 @@ +fixture `Fixture`; + +test('test', async t => { + await t.wait(10000); +}); diff --git a/test/functional/fixtures/video-recording/testcafe-fixtures/quarantine-test.js b/test/functional/fixtures/video-recording/testcafe-fixtures/quarantine-test.js new file mode 100644 index 00000000000..5ac1ad0acaa --- /dev/null +++ b/test/functional/fixtures/video-recording/testcafe-fixtures/quarantine-test.js @@ -0,0 +1,31 @@ +import { ClientFunction } from 'testcafe'; + +fixture `Video quarantine` + .page `../pages/index.html`; + +let counter = 0; + +const colors = { + 0: 'red', + 1: 'yellow', + 2: 'blue', + 3: 'green', +}; + +const changeBackground = ClientFunction(i => { + document.body.style.backgroundColor = colors[i]; +}, { dependencies: { colors } }); + +test(`quarantine with attempts`, async t => { + await changeBackground(counter); + await t.wait(1000); + + counter++; + + if (counter < 2) + throw new Error('error'); +}); + +test(`quarantine without attempts`, async () => { + await changeBackground(counter); +}); diff --git a/test/functional/get-test-error.js b/test/functional/get-test-error.js index 30217f66140..8a85e855417 100644 --- a/test/functional/get-test-error.js +++ b/test/functional/get-test-error.js @@ -1,6 +1,6 @@ function areErrorsSame (errors) { - for (var i = 0; i < errors.length; i++) { - for (var j = i + 1; j < errors.length; j++) { + for (let i = 0; i < errors.length; i++) { + for (let j = i + 1; j < errors.length; j++) { if (errors[i] !== errors[j]) return false; } @@ -15,7 +15,7 @@ function normalizeError (err, userAgents) { .map(function (str) { str = str.trim(); - return str.replace(/^Browser\:.+$/, '[[user-agent]]'); + return str.replace(/^Browser:.+$/, '[[user-agent]]'); }) .filter(function (str, idx) { // NOTE: remove user agent from legacy API errors @@ -31,7 +31,7 @@ function normalizeError (err, userAgents) { // If different errors occur in several browsers, a dictionary object is returned. In this case, browser aliases are // keys, and values are arrays of errors. module.exports = function getTestError (taskReport, browsers) { - var errs = []; + let errs = []; taskReport.fixtures.forEach(function (fixture) { fixture.tests.forEach(function (test) { @@ -42,11 +42,11 @@ module.exports = function getTestError (taskReport, browsers) { if (!errs.length) return null; - var userAgents = browsers.map(function (browserInfo) { + const userAgents = browsers.map(function (browserInfo) { return browserInfo.connection.userAgent; }); - var normalizedErrors = errs.map(function (err) { + const normalizedErrors = errs.map(function (err) { return normalizeError(err, userAgents); }); @@ -54,10 +54,10 @@ module.exports = function getTestError (taskReport, browsers) { return [normalizedErrors[0]]; if (browsers.length > 1) { - var testError = {}; + const testError = {}; browsers.forEach(function (browserInfo) { - var errorsArray = errs + const errorsArray = errs .filter(function (error) { return error.indexOf(browserInfo.connection.userAgent) > -1; }) diff --git a/test/functional/is-touch-device.js b/test/functional/is-touch-device.js index 6af1ec66990..047cbdb62cc 100644 --- a/test/functional/is-touch-device.js +++ b/test/functional/is-touch-device.js @@ -1,17 +1,17 @@ -/*eslint-disable no-unused-vars*/ +/*eslint-disable @typescript-eslint/no-unused-vars*/ function isTouchDevice () { - var userAgent = window.navigator.userAgent.toLocaleLowerCase(); - var mobile = /[^-]mobi/i.test(userAgent); - var tablet = /tablet/i.test(userAgent); - var nexusDevice = /nexus\s*[0-6]\s*/i.test(userAgent) || /nexus\s*[0-9]+/i.test(userAgent); - var blackberry = /blackberry|\bbb\d+/i.test(userAgent) || /rim\stablet/i.test(userAgent); - var isIOS = /(iphone|ipod|ipad)/.test(userAgent); - var isAndroid = /(android)/.test(userAgent); + const userAgent = window.navigator.userAgent.toLocaleLowerCase(); + const mobile = /[^-]mobi/i.test(userAgent); + const tablet = /tablet/i.test(userAgent); + const nexusDevice = /nexus\s*[0-6]\s*/i.test(userAgent) || /nexus\s*[0-9]+/i.test(userAgent); + const blackberry = /blackberry|\bbb\d+/i.test(userAgent) || /rim\stablet/i.test(userAgent); + const isIOS = /(iphone|ipod|ipad)/.test(userAgent); + const isAndroid = /(android)/.test(userAgent); - var isDevice = mobile || tablet || nexusDevice || isIOS || isAndroid || blackberry; - var hasTouchEvents = 'ontouchstart' in window; + const isDevice = mobile || tablet || nexusDevice || isIOS || isAndroid || blackberry; + const hasTouchEvents = 'ontouchstart' in window; return isDevice && hasTouchEvents; } -/*eslint-disable no-unused-vars*/ +/*eslint-disable @typescript-eslint/no-unused-vars*/ diff --git a/test/functional/legacy-fixtures/.eslintrc b/test/functional/legacy-fixtures/.eslintrc index 65de2113fb4..a7194f8290b 100644 --- a/test/functional/legacy-fixtures/.eslintrc +++ b/test/functional/legacy-fixtures/.eslintrc @@ -1,6 +1,7 @@ { "rules": { - "no-unused-expressions": 0 + "no-unused-expressions": 0, + "no-var": 0 }, "globals": { "handleConfirm": false, @@ -9,4 +10,4 @@ "handlePrompt": false, "getInput": true } -} \ No newline at end of file +} diff --git a/test/functional/legacy-fixtures/api/click/test.js b/test/functional/legacy-fixtures/api/click/test.js index b6b8f49a5ff..8d64e30f83e 100644 --- a/test/functional/legacy-fixtures/api/click/test.js +++ b/test/functional/legacy-fixtures/api/click/test.js @@ -7,9 +7,9 @@ describe('[Legacy API] act.click()', function () { .catch(function (errs) { var expectedError = [ 'Error at step "1.Click on invisible element":', - 'A target element \ of the click action is not visible.', + 'A target element of the click action is not visible.', 'If this element should appear when you are hovering over another', - 'element, make sure that you properly recorded the hover action.' + 'element, make sure that you properly recorded the hover action.', ].join(' '); expect(errs[0]).contains(expectedError); @@ -22,17 +22,13 @@ describe('[Legacy API] act.click()', function () { .catch(function (errs) { var expectedError = [ 'Error at step "1.Click on invisible element":', - 'A target element \ of the click action is not visible.', + 'A target element of the click action is not visible.', 'If this element should appear when you are hovering over another', - 'element, make sure that you properly recorded the hover action.' + 'element, make sure that you properly recorded the hover action.', ].join(' '); expect(errs[0]).contains(expectedError); expect(errs[0]).contains('act.click($input);'); }); }); - - it('Pointer events test (T191183) [ONLY:ie]', function () { - return runTests('testcafe-fixtures/click.test.js', 'Pointer events test (T191183)', { only: 'ie' }); - }); }); diff --git a/test/functional/legacy-fixtures/api/click/testcafe-fixtures/click.test.js b/test/functional/legacy-fixtures/api/click/testcafe-fixtures/click.test.js index 8b497454614..8a497d5abca 100644 --- a/test/functional/legacy-fixtures/api/click/testcafe-fixtures/click.test.js +++ b/test/functional/legacy-fixtures/api/click/testcafe-fixtures/click.test.js @@ -2,51 +2,12 @@ '@page ./index.html'; -var userAgent = window.navigator.userAgent.toLowerCase(); -var isMSEdge = !!/edge\//.test(userAgent); - -var isIE11 = !!(navigator.appCodeName === 'Mozilla' && - /trident\/7.0/.test(userAgent)); - - -'@test'['Pointer events test (T191183)'] = { - '1.Bind pointer event handlers and call click': function () { - var input = $('#input')[0]; - var shared = this; - - shared.events = []; - - function pointerHandler (e) { - shared.events.push(e.type.toLowerCase().replace('ms', '')); - - eq(e.pointerType, isIE11 || isMSEdge ? 'mouse' : 4); - eq(e.button, 0); - eq(e.buttons, 1); - } - - if (isMSEdge) { - input.onpointerdown = pointerHandler; - input.onpointerup = pointerHandler; - } - else { - input.onmspointerdown = pointerHandler; - input.onmspointerup = pointerHandler; - } - - act.click(input); - }, - - '2.Check that handlers were called': function () { - eq(this.events, ['pointerdown', 'pointerup']); - } -}; - '@test'['Should fail if the first argument is invisible'] = { '1.Click on invisible element': function () { var $input = $('#input').css('visibility', 'hidden'); act.click($input); - } + }, }; '@test'['Should fail if the first argument is out of the visible area'] = { @@ -54,9 +15,9 @@ var isIE11 = !!(navigator.appCodeName === 'Mozilla' && var $input = $('#input').css({ position: 'absolute', left: '-200px', - top: '-200px' + top: '-200px', }); act.click($input); - } + }, }; diff --git a/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-correct-credentials.test.js b/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-correct-credentials.test.js index 1deb0e2d4e4..2aada361620 100644 --- a/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-correct-credentials.test.js +++ b/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-correct-credentials.test.js @@ -7,5 +7,5 @@ '@test'['Authenticate with correct credintials'] = { 'Step 1': function () { eq($('#result')[0].innerText, 'authorized'); - } + }, }; diff --git a/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-wrong-credentials.test.js b/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-wrong-credentials.test.js index aaafc06c82e..c75d4f98531 100644 --- a/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-wrong-credentials.test.js +++ b/test/functional/legacy-fixtures/auth/testcafe-fixtures/basic-auth-with-wrong-credentials.test.js @@ -7,5 +7,5 @@ '@test'['Authenticate with wrong credentials'] = { 'Step 1': function () { eq($('#result')[0].innerText, 'not authorized'); - } + }, }; diff --git a/test/functional/legacy-fixtures/auth/testcafe-fixtures/ntlm-auth-check-username.test.js b/test/functional/legacy-fixtures/auth/testcafe-fixtures/ntlm-auth-check-username.test.js index 9746e77b9d1..acbc578c210 100644 --- a/test/functional/legacy-fixtures/auth/testcafe-fixtures/ntlm-auth-check-username.test.js +++ b/test/functional/legacy-fixtures/auth/testcafe-fixtures/ntlm-auth-check-username.test.js @@ -9,5 +9,5 @@ var credentials = JSON.parse(text); eq(credentials.UserName, 'username'); - } + }, }; diff --git a/test/functional/legacy-fixtures/before-unload-handling/pages/index.html b/test/functional/legacy-fixtures/before-unload-handling/pages/index.html index 0ba8ead2c7c..c8bf3337a26 100644 --- a/test/functional/legacy-fixtures/before-unload-handling/pages/index.html +++ b/test/functional/legacy-fixtures/before-unload-handling/pages/index.html @@ -4,9 +4,15 @@ Index -This page + - - \ No newline at end of file diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-16/pages/index.html b/test/functional/legacy-fixtures/regression/legacy-gh-16/pages/index.html deleted file mode 100644 index c8efb4dc28f..00000000000 --- a/test/functional/legacy-fixtures/regression/legacy-gh-16/pages/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - Legacy GH-16 - - - - - \ No newline at end of file diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-16/test.js b/test/functional/legacy-fixtures/regression/legacy-gh-16/test.js deleted file mode 100644 index 67ebea4e119..00000000000 --- a/test/functional/legacy-fixtures/regression/legacy-gh-16/test.js +++ /dev/null @@ -1,5 +0,0 @@ -describe('[Regression](GH-16)', function () { - it('Should correctly store shared data in IE', function () { - return runTests('testcafe-fixtures/index.test.js', 'Check stored data', { only: 'ie' }); - }); -}); diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-16/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/legacy-gh-16/testcafe-fixtures/index.test.js deleted file mode 100644 index fc502471916..00000000000 --- a/test/functional/legacy-fixtures/regression/legacy-gh-16/testcafe-fixtures/index.test.js +++ /dev/null @@ -1,15 +0,0 @@ -'@fixture GH-16'; -'@page ./index.html'; - - -'@test'['Check stored data'] = { - '1.Set data and reload iframe': inIFrame('#iframe', function () { - this.data = 200; - - act.click('#replaceSrc'); - }), - - '2.Check data': function () { - eq(this.data, 200); - } -}; diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-16/testcafe-fixtures/test_config.json b/test/functional/legacy-fixtures/regression/legacy-gh-16/testcafe-fixtures/test_config.json deleted file mode 100644 index 3e1ab56adc7..00000000000 --- a/test/functional/legacy-fixtures/regression/legacy-gh-16/testcafe-fixtures/test_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseUrl": "./regression/legacy-gh-16/pages/" -} \ No newline at end of file diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-42/pages/index.html b/test/functional/legacy-fixtures/regression/legacy-gh-42/pages/index.html new file mode 100644 index 00000000000..0fc2ea961a8 --- /dev/null +++ b/test/functional/legacy-fixtures/regression/legacy-gh-42/pages/index.html @@ -0,0 +1,16 @@ + + + + Legacy GH-42 + + + + + + diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-42/test.js b/test/functional/legacy-fixtures/regression/legacy-gh-42/test.js new file mode 100644 index 00000000000..0cd66744f86 --- /dev/null +++ b/test/functional/legacy-fixtures/regression/legacy-gh-42/test.js @@ -0,0 +1,3 @@ +it('[Regression] (Legacy GH-42) Open/write in iframe without src', function () { + return runTests('testcafe-fixtures/index.test.js'); +}); diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-42/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/legacy-gh-42/testcafe-fixtures/index.test.js new file mode 100644 index 00000000000..b5bdd5a57aa --- /dev/null +++ b/test/functional/legacy-fixtures/regression/legacy-gh-42/testcafe-fixtures/index.test.js @@ -0,0 +1,9 @@ +'@fixture legacy-gh-42'; +'@page ./index.html'; + + +'@test'['Open/write in iframe without src'] = { + '1.Check iframe': inIFrame('#iframe', function () { + eq($('#label').text(), 'hello'); + }), +}; diff --git a/test/functional/legacy-fixtures/regression/legacy-gh-42/testcafe-fixtures/test_config.json b/test/functional/legacy-fixtures/regression/legacy-gh-42/testcafe-fixtures/test_config.json new file mode 100644 index 00000000000..c4def7d9d90 --- /dev/null +++ b/test/functional/legacy-fixtures/regression/legacy-gh-42/testcafe-fixtures/test_config.json @@ -0,0 +1,3 @@ +{ + "baseUrl": "./regression/legacy-gh-42/pages/" +} diff --git a/test/functional/legacy-fixtures/regression/prevent-real-action/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/prevent-real-action/testcafe-fixtures/index.test.js index fb4f3f3598d..bead819aa11 100644 --- a/test/functional/legacy-fixtures/regression/prevent-real-action/testcafe-fixtures/index.test.js +++ b/test/functional/legacy-fixtures/regression/prevent-real-action/testcafe-fixtures/index.test.js @@ -19,5 +19,5 @@ // NOTE: We simulate a click performed by a user during TestCafe test execution. If TestCafe // doesn't prevent a click, test will fail with an unexpected alert dialog error. window['%hammerhead%'].nativeMethods.click.call(document.getElementById('alertDiv')); - }) + }), }; diff --git a/test/functional/legacy-fixtures/regression/t116171/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/t116171/testcafe-fixtures/index.test.js index a1294a7f440..df09dc75c80 100644 --- a/test/functional/legacy-fixtures/regression/t116171/testcafe-fixtures/index.test.js +++ b/test/functional/legacy-fixtures/regression/t116171/testcafe-fixtures/index.test.js @@ -27,5 +27,5 @@ '6.Check result in the iFrame': inIFrame($('#iframe'), function () { eq(window.blackClick, 1); eq(window.redClick, 1); - }) + }), }; diff --git a/test/functional/legacy-fixtures/regression/t171129/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/t171129/testcafe-fixtures/index.test.js index 13e537860de..a756a16a81b 100644 --- a/test/functional/legacy-fixtures/regression/t171129/testcafe-fixtures/index.test.js +++ b/test/functional/legacy-fixtures/regression/t171129/testcafe-fixtures/index.test.js @@ -9,5 +9,5 @@ '2.Click div2': function () { act.click('#div2'); - } + }, }; diff --git a/test/functional/legacy-fixtures/regression/t174562/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/t174562/testcafe-fixtures/index.test.js index 60051f0f5d1..a09554b2446 100644 --- a/test/functional/legacy-fixtures/regression/t174562/testcafe-fixtures/index.test.js +++ b/test/functional/legacy-fixtures/regression/t174562/testcafe-fixtures/index.test.js @@ -14,5 +14,5 @@ '3.Assert': inIFrame($('#iframe'), function () { ok(window.divClicked); ok($(window).scrollTop() > 0); - }) + }), }; diff --git a/test/functional/legacy-fixtures/regression/t190110/pages/first-page.html b/test/functional/legacy-fixtures/regression/t190110/pages/first-page.html deleted file mode 100644 index 4a1350d9520..00000000000 --- a/test/functional/legacy-fixtures/regression/t190110/pages/first-page.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - T190110 - first page - - -
T190110 - first page
-Link - - \ No newline at end of file diff --git a/test/functional/legacy-fixtures/regression/t190110/pages/second-page.html b/test/functional/legacy-fixtures/regression/t190110/pages/second-page.html deleted file mode 100644 index 1fe71ec11f8..00000000000 --- a/test/functional/legacy-fixtures/regression/t190110/pages/second-page.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - T190110 - second page - - -
T190110 - second page
- - \ No newline at end of file diff --git a/test/functional/legacy-fixtures/regression/t190110/test.js b/test/functional/legacy-fixtures/regression/t190110/test.js deleted file mode 100644 index eb99778f2aa..00000000000 --- a/test/functional/legacy-fixtures/regression/t190110/test.js +++ /dev/null @@ -1,3 +0,0 @@ -it('[Regression](T190110) The next step is skipped if a server responds too long after clicking a link in the previous step in IE', function () { - return runTests('testcafe-fixtures/index.test.js'); -}); diff --git a/test/functional/legacy-fixtures/regression/t190110/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/t190110/testcafe-fixtures/index.test.js deleted file mode 100644 index 7a2348fee88..00000000000 --- a/test/functional/legacy-fixtures/regression/t190110/testcafe-fixtures/index.test.js +++ /dev/null @@ -1,13 +0,0 @@ -'@fixture T190110'; -'@page ./first-page.html'; - - -'@test'['T190110'] = { - '1.Click link "Link"': function () { - act.click('#link'); - }, - - '2.Assert': function () { - eq($('#pageName').text(), 'T190110 - second page'); - } -}; diff --git a/test/functional/legacy-fixtures/regression/t190110/testcafe-fixtures/test_config.json b/test/functional/legacy-fixtures/regression/t190110/testcafe-fixtures/test_config.json deleted file mode 100644 index 6e8a1ff1671..00000000000 --- a/test/functional/legacy-fixtures/regression/t190110/testcafe-fixtures/test_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseUrl": "./regression/t190110/pages/" -} \ No newline at end of file diff --git a/test/functional/legacy-fixtures/regression/t200501/test.js b/test/functional/legacy-fixtures/regression/t200501/test.js index a6683483093..5f22e4b8caa 100644 --- a/test/functional/legacy-fixtures/regression/t200501/test.js +++ b/test/functional/legacy-fixtures/regression/t200501/test.js @@ -6,7 +6,7 @@ it('[Regression](T200501) Wait parameters are not verified', function () { .catch(function (errs) { var expectedError = [ 'Error at step "1.Wait with mixed up parameters":', - 'wait action\'s "milliseconds" parameter should be a positive number.' + 'wait action\'s "milliseconds" parameter should be a positive number.', ].join(' '); expect(errs[0]).contains(expectedError); diff --git a/test/functional/legacy-fixtures/regression/t200501/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/t200501/testcafe-fixtures/index.test.js index 4df3aed6a29..24ea5157ee6 100644 --- a/test/functional/legacy-fixtures/regression/t200501/testcafe-fixtures/index.test.js +++ b/test/functional/legacy-fixtures/regression/t200501/testcafe-fixtures/index.test.js @@ -7,5 +7,5 @@ act.wait(function () { return false; }, 500); - } + }, }; diff --git a/test/functional/legacy-fixtures/regression/t212974/pages/index.html b/test/functional/legacy-fixtures/regression/t212974/pages/index.html deleted file mode 100644 index 3f3947859e9..00000000000 --- a/test/functional/legacy-fixtures/regression/t212974/pages/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - T212974 - index page - - -
target element
-
- - - \ No newline at end of file diff --git a/test/functional/legacy-fixtures/regression/t212974/test.js b/test/functional/legacy-fixtures/regression/t212974/test.js deleted file mode 100644 index c17d3e97c4d..00000000000 --- a/test/functional/legacy-fixtures/regression/t212974/test.js +++ /dev/null @@ -1,3 +0,0 @@ -it("[Regression](T212974) Page script can't get element under TestCafe cursor with document.elementFromPoint in IE", function () { - return runTests('testcafe-fixtures/index.test.js'); -}); diff --git a/test/functional/legacy-fixtures/regression/t212974/testcafe-fixtures/index.test.js b/test/functional/legacy-fixtures/regression/t212974/testcafe-fixtures/index.test.js deleted file mode 100644 index 97d7fb13a13..00000000000 --- a/test/functional/legacy-fixtures/regression/t212974/testcafe-fixtures/index.test.js +++ /dev/null @@ -1,13 +0,0 @@ -'@fixture T212974'; -'@page ./index.html'; - - -'@test'['T212974'] = { - '1.Click div "target element"': function () { - act.click('#targetElement'); - }, - - '2.Assert': function () { - eq($('#elementFromPoint').text(), $('#targetElement').attr('id')); - } -}; diff --git a/test/functional/legacy-fixtures/regression/t212974/testcafe-fixtures/test_config.json b/test/functional/legacy-fixtures/regression/t212974/testcafe-fixtures/test_config.json deleted file mode 100644 index d7657c51e95..00000000000 --- a/test/functional/legacy-fixtures/regression/t212974/testcafe-fixtures/test_config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "baseUrl": "./regression/t212974/pages/" -} \ No newline at end of file diff --git a/test/functional/legacy-fixtures/screenshots/test.js b/test/functional/legacy-fixtures/screenshots/test.js new file mode 100644 index 00000000000..fd4d73be5b6 --- /dev/null +++ b/test/functional/legacy-fixtures/screenshots/test.js @@ -0,0 +1,26 @@ +const { expect } = require('chai'); +const assertionHelper = require('../../assertion-helper'); + +const SCREENSHOT_PATH_MESSAGE_RE = /___test-screenshots___[\\/]\d{4,4}-\d{2,2}-\d{2,2}_\d{2,2}-\d{2,2}-\d{2,2}[\\/]test-1/; +const ERROR_SCREENSHOT_PATH_RE = /Screenshot: .*?___test-screenshots___[\\/]\d{4,4}-\d{2,2}-\d{2,2}_\d{2,2}-\d{2,2}-\d{2,2}[\\/]test-1[\\/]\S+[\\/]errors[\\/]\d.png/; + +describe('Screenshots', () => { + afterEach(assertionHelper.removeScreenshotDir); + + it('Should take a screenshot', () => { + return runTests('./testcafe-fixtures/screenshots.test.js', 'Take a screenshot', { setScreenshotPath: true }) + .then(() => { + expect(SCREENSHOT_PATH_MESSAGE_RE.test(testReport.screenshotPath)).eql(true); + expect(assertionHelper.checkScreenshotsCreated({ forError: false, screenshotsCount: 2 })).eql(true); + }); + }); + + it('Should take a screenshot if an error in test code is raised', () => { + return runTests('./testcafe-fixtures/screenshots.test.js', 'Screenshot on test code error', + { shouldFail: true, screenshotsOnFails: true, setScreenshotPath: true }) + .catch(errs => { + assertionHelper.errorInEachBrowserContainsRegExp(errs, ERROR_SCREENSHOT_PATH_RE, 0); + expect(assertionHelper.checkScreenshotsCreated({ forError: true })).eql(true); + }); + }); +}); diff --git a/test/functional/legacy-fixtures/screenshots/testcafe-fixtures/screenshots.test.js b/test/functional/legacy-fixtures/screenshots/testcafe-fixtures/screenshots.test.js new file mode 100644 index 00000000000..0803faf8898 --- /dev/null +++ b/test/functional/legacy-fixtures/screenshots/testcafe-fixtures/screenshots.test.js @@ -0,0 +1,15 @@ +'@fixture click'; +'@page http://example.com'; + +'@test'['Take a screenshot'] = { + '1.Click on non-existing element': function () { + act.screenshot(); + }, +}; + + +'@test'['Screenshot on test code error'] = { + '1.Click on non-existing element': function () { + throw new Error('STOP'); + }, +}; diff --git a/test/functional/legacy-fixtures/selector-timeout/test.js b/test/functional/legacy-fixtures/selector-timeout/test.js index c995ba8c6af..b3276727e4b 100644 --- a/test/functional/legacy-fixtures/selector-timeout/test.js +++ b/test/functional/legacy-fixtures/selector-timeout/test.js @@ -7,9 +7,9 @@ describe('Selector timeout', function () { .catch(function (errs) { var expectedError = [ 'Error at step "2.Click on button":', - 'A target element \ - - + +
- +
@@ -37,6 +37,7 @@ input.id = 'addedInput'; input.multiple = true; input.type = 'file'; + input.name = 'file'; parent.appendChild(input); } diff --git a/test/functional/legacy-fixtures/upload/testcafe-fixtures/mixin.js b/test/functional/legacy-fixtures/upload/testcafe-fixtures/mixin.js index b40ad0d3eeb..443e10abc4a 100644 --- a/test/functional/legacy-fixtures/upload/testcafe-fixtures/mixin.js +++ b/test/functional/legacy-fixtures/upload/testcafe-fixtures/mixin.js @@ -15,18 +15,18 @@ var isTextPresentOnPage = function (text) { '@mixin'['Check text1.txt upload'] = { 'Check text1 on the page': function () { ok(isTextPresentOnPage('London is the capital of Great Britain')); - } + }, }; '@mixin'['Check text2.txt upload'] = { 'Check text2 on the page': function () { ok(isTextPresentOnPage('The city is very old and beautiful.')); - } + }, }; '@mixin'['Check empty upload'] = { 'Check missing text1 and text2 on the page': function () { ok(!isTextPresentOnPage('London is the capital of Great Britain')); ok(!isTextPresentOnPage('The city is very old and beautiful.')); - } + }, }; diff --git a/test/functional/legacy-fixtures/upload/testcafe-fixtures/regression.test.js b/test/functional/legacy-fixtures/upload/testcafe-fixtures/regression.test.js index a59e9930945..c251925b198 100644 --- a/test/functional/legacy-fixtures/upload/testcafe-fixtures/regression.test.js +++ b/test/functional/legacy-fixtures/upload/testcafe-fixtures/regression.test.js @@ -17,7 +17,7 @@ act.click('#submitBtn'); }, - '4. Check text1 upload': '@mixin Check text1.txt upload' + '4. Check text1 upload': '@mixin Check text1.txt upload', }; '@test'['Upload by using a replaced element'] = { @@ -37,7 +37,7 @@ act.click('#submitBtn'); }, - '5. Check text1 upload': '@mixin Check text1.txt upload' + '5. Check text1 upload': '@mixin Check text1.txt upload', }; '@test'['Upload by using a removed element'] = { @@ -53,5 +53,5 @@ act.click('#submitBtn'); }, - '4. Check empty upload': '@mixin Check empty upload' + '4. Check empty upload': '@mixin Check empty upload', }; diff --git a/test/functional/legacy-fixtures/upload/testcafe-fixtures/upload.test.js b/test/functional/legacy-fixtures/upload/testcafe-fixtures/upload.test.js index 55fe13642bb..fe11a0ccda0 100644 --- a/test/functional/legacy-fixtures/upload/testcafe-fixtures/upload.test.js +++ b/test/functional/legacy-fixtures/upload/testcafe-fixtures/upload.test.js @@ -14,7 +14,7 @@ act.click('#submitBtn'); }, - '3. Check text1 upload': '@mixin Check text1.txt upload' + '3. Check text1 upload': '@mixin Check text1.txt upload', }; '@test'['Upload multiple files'] = { @@ -30,7 +30,7 @@ '3. Check text1 upload': '@mixin Check text1.txt upload', - '3. Check text2 upload': '@mixin Check text2.txt upload' + '3. Check text2 upload': '@mixin Check text2.txt upload', }; '@test'['Clear upload'] = { @@ -48,7 +48,7 @@ act.click('#submitBtn'); }, - '4. Check that text1 is not uploaded': '@mixin Check empty upload' + '4. Check that text1 is not uploaded': '@mixin Check empty upload', }; '@test'['Upload a non-existent file - should fail'] = { @@ -58,7 +58,7 @@ act.upload('#fileInput', file); }, - '2. Check empty upload': '@mixin Check empty upload' + '2. Check empty upload': '@mixin Check empty upload', }; '@test'['Upload multiple files inc. non-existent - should fail'] = { @@ -68,5 +68,5 @@ act.upload('#fileInput', files); }, - '2. Check empty upload': '@mixin Check empty upload' + '2. Check empty upload': '@mixin Check empty upload', }; diff --git a/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/helpers.js b/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/helpers.js index 6baefadacd1..a309c076e81 100644 --- a/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/helpers.js +++ b/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/helpers.js @@ -1,5 +1,5 @@ -/*eslint-disable no-unused-vars*/ +/*eslint-disable @typescript-eslint/no-unused-vars*/ function getInput () { return $('#input'); } -/*eslint-enable no-unused-vars*/ +/*eslint-enable @typescript-eslint/no-unused-vars*/ diff --git a/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/mixins.js b/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/mixins.js index 0ec9dd1ca84..81d671f548f 100644 --- a/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/mixins.js +++ b/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/mixins.js @@ -1,5 +1,5 @@ '@mixin'['Type in input in iframe'] = { '1.Type in input': inIFrame('#iframe', function () { act.type('#input', this.text); - }) + }), }; diff --git a/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/shared-code.test.js b/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/shared-code.test.js index f2bf028f90b..5365b0b0bb9 100644 --- a/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/shared-code.test.js +++ b/test/functional/legacy-fixtures/using-shared-code/testcafe-fixtures/shared-code.test.js @@ -12,7 +12,7 @@ var basePath = 'http://localhost:3000/legacy-fixtures/using-shared-code/pages/'; '@testCases': [ { '@name': 'Cross-domain iframe', pageUrl: 'cross-domain.html' }, { '@name': 'Same-domain iframe', pageUrl: 'same-domain.html' }, - { '@name': 'Inline iframe', pageUrl: 'inline.html' } + { '@name': 'Inline iframe', pageUrl: 'inline.html' }, ], '1.Navigate to test page': function () { @@ -27,14 +27,14 @@ var basePath = 'http://localhost:3000/legacy-fixtures/using-shared-code/pages/'; '3.Check input value': inIFrame('#iframe', function () { eq($('#input')[0].value, this.text); - }) + }), }; '@test'['Using mixin in iframe'] = { '@testCases': [ { '@name': 'Cross-domain iframe', pageUrl: 'cross-domain.html' }, { '@name': 'Same-domain iframe', pageUrl: 'same-domain.html' }, - { '@name': 'Inline iframe', pageUrl: 'inline.html' } + { '@name': 'Inline iframe', pageUrl: 'inline.html' }, ], '1.Navigate to test page': function () { @@ -47,5 +47,5 @@ var basePath = 'http://localhost:3000/legacy-fixtures/using-shared-code/pages/'; '3.Check input value': inIFrame('#iframe', function () { eq($('#input')[0].value, this.text); - }) + }), }; diff --git a/test/functional/quarantine-mode-tracker.js b/test/functional/quarantine-mode-tracker.js index 2e2ddaba288..94312f1b26e 100644 --- a/test/functional/quarantine-mode-tracker.js +++ b/test/functional/quarantine-mode-tracker.js @@ -4,7 +4,7 @@ module.exports = { handleFailingSequence: function (ua) { // NOTE: Failing sequence: 1st run - fail, 2nd - pass, 3rd and 4th - fail - var state = this.browsersFailingQuarantine[ua] || { step: 0 }; + const state = this.browsersFailingQuarantine[ua] || { step: 0 }; state.step++; @@ -18,7 +18,7 @@ module.exports = { handlePassingSequence: function (ua) { // NOTE: Passing sequence: 1st run - fail, 2nd, 3rd and 4th - pass - var state = this.browsersPassingQuarantine[ua] || { step: 0 }; + const state = this.browsersPassingQuarantine[ua] || { step: 0 }; state.step++; @@ -36,5 +36,5 @@ module.exports = { clearFailingBrowsers: function () { this.browsersFailingQuarantine = {}; - } + }, }; diff --git a/test/functional/remote-connector.js b/test/functional/remote-connector.js new file mode 100644 index 00000000000..fcc8191920a --- /dev/null +++ b/test/functional/remote-connector.js @@ -0,0 +1,29 @@ +const qrCode = require('qrcode-terminal'); + + +module.exports = class RemoteConnector { + connect () { + return Promise.resolve(); + } + + waitForFreeMachines () { + return Promise.resolve(); + } + + startBrowser (settings, url) { + console.log('Connection URL:', url); //eslint-disable-line no-console + + if (settings.qrCode) + qrCode.generate(url); + + return Promise.resolve(); + } + + stopBrowser () { + return Promise.resolve(); + } + + disconnect () { + return Promise.resolve(); + } +}; diff --git a/test/functional/setup.js b/test/functional/setup.js index ca8ff328244..153e7591b1f 100644 --- a/test/functional/setup.js +++ b/test/functional/setup.js @@ -1,19 +1,28 @@ -var browserTools = require('testcafe-browser-tools'); -var SlConnector = require('saucelabs-connector'); -var BsConnector = require('browserstack-connector'); -var Promise = require('pinkie'); -var caller = require('caller'); -var path = require('path'); -var createTestCafe = require('../../lib'); -var config = require('./config.js'); -var site = require('./site'); -var getTestError = require('./get-test-error.js'); - -var testCafe = null; -var browsersInfo = null; - -var connector = null; -var browserInstances = null; +const path = require('path'); +const BsConnector = require('browserstack-connector'); +const caller = require('caller'); +const promisifyEvent = require('promisify-event'); +const createTestCafe = require('../../lib'); +const browserProviderPool = require('../../lib/browser/provider/pool'); +const BrowserConnection = require('../../lib/browser/connection'); +const config = require('./config.js'); +const site = require('./site'); +const RemoteConnector = require('./remote-connector'); +const getTestError = require('./get-test-error.js'); +const { createSimpleTestStream } = require('./utils/stream'); +const BrowserConnectionStatus = require('../../lib/browser/connection/status'); +const osFamily = require('os-family'); +const { findWindow, errors } = require('testcafe-browser-tools'); +const authenticationHelper = require('../../lib/cli/authentication-helper'); +const isCI = require('is-ci'); + +const setNativeAutomationForRemoteConnection = require('./utils/set-native-automation-for-remote-connection'); + +let testCafe = null; +let browsersInfo = null; + +let connector = null; +let browserInstances = null; const WAIT_FOR_FREE_MACHINES_REQUEST_INTERVAL = 60000; const WAIT_FOR_FREE_MACHINES_MAX_ATTEMPT_COUNT = 60; @@ -22,23 +31,85 @@ const BROWSER_OPENING_TIMEOUT = 90000; const FUNCTIONAL_TESTS_SELECTOR_TIMEOUT = 200; const FUNCTIONAL_TESTS_ASSERTION_TIMEOUT = 1000; +const FUNCTIONAL_TESTS_PAGE_LOAD_TIMEOUT = 0; -var envName = process.env.TESTING_ENVIRONMENT || config.testingEnvironmentNames.localBrowsers; -var environment = config.testingEnvironments[envName]; -var browserProvider = process.env.BROWSER_PROVIDER; -var isBrowserStack = browserProvider === config.browserProviderNames.browserstack; +const environment = config.currentEnvironment; +const browserProvider = config.currentEnvironment.provider; +const isBrowserStack = browserProvider === config.browserProviderNames.browserstack; config.browsers = environment.browsers; const REQUESTED_MACHINES_COUNT = environment.browsers.length; +const REMOTE_CONNECTORS_MAP = { + [config.browserProviderNames.browserstack]: BsConnector, + [config.browserProviderNames.remote]: RemoteConnector, +}; + +const USE_PROVIDER_POOL = config.useLocalBrowsers || isBrowserStack; + +async function hasLocalBrowsers (browserInfo) { + for (const browser of browserInfo) { + if (browser instanceof BrowserConnection) + continue; + + if (await browser.provider.isLocalBrowser(void 0, browser.browserName)) + return true; + } + + return false; +} + +async function createTestcafeBrowserTools (browserInfo) { + const hasLocal = await hasLocalBrowsers([browserInfo]); + + const { error } = await authenticationHelper( + () => findWindow(''), + errors.UnableToAccessScreenRecordingAPIError, + { + interactive: hasLocal && !isCI, + }, + ); + + if (!error) + return; + + if (hasLocal) + throw error; +} + function getBrowserInfo (settings) { - return testCafe - .createBrowserConnection() - .then(function (connection) { + return Promise + .resolve() + .then(() => { + if (osFamily.mac) { + return browserProviderPool + .getBrowserInfo(settings.browserName) + .then((browserInfo) => createTestcafeBrowserTools(browserInfo)); + } + + return Promise.resolve(); + }) + .then(() => { + if (!USE_PROVIDER_POOL) + return testCafe.createBrowserConnection(); + + return browserProviderPool + .getBrowserInfo(settings.browserName) + .then(browserInfo => { + const options = { + disableMultipleWindows: false, + nativeAutomation: config.nativeAutomation, + developmentMode: config.devMode, + }; + + return new BrowserConnection(testCafe.browserConnectionGateway, browserInfo, true, options); + }); + }) + .then(connection => { return { settings: settings, - connection: connection + connection: connection, }; }); } @@ -46,142 +117,209 @@ function getBrowserInfo (settings) { function initBrowsersInfo () { return Promise .all(environment.browsers.map(getBrowserInfo)) - .then(function (info) { + .then(info => { browsersInfo = info; }); } function openRemoteBrowsers () { - var Connector = isBrowserStack ? BsConnector : SlConnector; + const Connector = REMOTE_CONNECTORS_MAP[browserProvider]; connector = new Connector(environment[browserProvider].username, environment[browserProvider].accessKey, { servicePort: config.browserstackConnectorServicePort }); return connector .connect() - .then(function () { + .then(() => { return connector.waitForFreeMachines(REQUESTED_MACHINES_COUNT, WAIT_FOR_FREE_MACHINES_REQUEST_INTERVAL, WAIT_FOR_FREE_MACHINES_MAX_ATTEMPT_COUNT); }) - .then(function () { - var buildInfo = { + .then(() => { + const buildInfo = { jobName: environment.jobName, build: process.env.TRAVIS_BUILD_ID || '', - tags: [process.env.TRAVIS_BRANCH || 'master'] + tags: [process.env.TRAVIS_BRANCH || 'master'], }; - var openBrowserPromises = browsersInfo.map(function (browserInfo) { + const openBrowserPromises = browsersInfo.map(browserInfo => { return connector.startBrowser(browserInfo.settings, browserInfo.connection.url, buildInfo, isBrowserStack ? { openingTimeout: BROWSER_OPENING_TIMEOUT } : null); }); return Promise.all(openBrowserPromises); }) - .then(function (browsers) { + .then(browsers => { browserInstances = browsers; }); } -function openLocalBrowsers () { - var openBrowserPromises = browsersInfo.map(function (browserInfo) { - return browserTools.getBrowserInfo(browserInfo.settings.alias) - .then(function (browser) { - return browserTools.open(browser, browserInfo.connection.url); - }); - }); +function waitUtilBrowserConnectionOpened (connection) { + if (connection.status === BrowserConnectionStatus.uninitialized) + connection.initialize(); + + const connectedPromise = connection.status === BrowserConnectionStatus.opened + ? Promise.resolve() + : promisifyEvent(connection, 'opened'); - return Promise.all(openBrowserPromises); + return connectedPromise + .then(() => { + // eslint-disable-next-line no-console + console.log(`Connected ${connection.userAgent}`); + }); } -function closeRemoteBrowsers () { - var closeBrowserPromises = browserInstances.map(function (browser) { - return connector.stopBrowser(isBrowserStack ? browser.id : browser); - }); +function waitUntilBrowsersConnected () { + return Promise.all(browsersInfo.map(browserInfo => waitUtilBrowserConnectionOpened(browserInfo.connection))); +} - return Promise.all(closeBrowserPromises) - .then(function () { - return connector.disconnect(); - }); +async function closeRemoteBrowsers () { + const closeBrowserPromises = browserInstances.map(browser => connector.stopBrowser(isBrowserStack ? browser.id : browser)); + + await Promise.all(closeBrowserPromises); + + await connector.disconnect(); } function closeLocalBrowsers () { - var closeBrowserPromises = browsersInfo.map(function (browserInfo) { - return browserInfo.connection.getStatus().then(function (status) { - return browserTools.close(status.url); - }); + const closeBrowserPromises = browsersInfo.map(browserInfo => { + browserInfo.connection.close(); + + return promisifyEvent(browserInfo.connection, 'closed'); }); return Promise.all(closeBrowserPromises); } before(function () { - var mocha = this; + const mocha = this; + + mocha.timeout(60000); + + const { devMode, retryTestPages } = config; - return createTestCafe(config.testCafe.hostname, config.testCafe.port1, config.testCafe.port2) + const testCafeOptions = { + hostname: config.testCafe.hostname, + port1: config.testCafe.port1, + port2: config.testCafe.port2, + + developmentMode: devMode, + + retryTestPages, + userVariables: { + url: 'localhost', + port: 1337, + isUserVariables: true, + }, + esm: config.esm, + }; + + return createTestCafe(testCafeOptions) .then(function (tc) { testCafe = tc; return initBrowsersInfo(); }) - .then(function () { - var aliases = browsersInfo.map(function (browser) { - return browser.settings.alias; - }); + .then(() => { + const aliases = browsersInfo.map(browser => browser.settings.alias); process.stdout.write('Running tests in browsers: ' + aliases.join(', ') + '\n'); site.create(config.site.ports, config.site.viewsPath); - if (!config.useLocalBrowsers) { - // NOTE: we need to disable this particular timeout for preventing mocha timeout - // error while establishing connection to Sauce Labs. If connection wouldn't be - // established after a specified number of attempts, an error will be thrown. + // NOTE: we need to disable this particular timeout for preventing mocha timeout + // error while establishing connection to Sauce Labs. If connection wouldn't be + // established after a specified number of attempts, an error will be thrown. + if (isBrowserStack || !USE_PROVIDER_POOL) mocha.timeout(0); - return openRemoteBrowsers(); + if (USE_PROVIDER_POOL) { + testCafe.configuration.mergeOptions({ + disableNativeAutomation: !config.nativeAutomation, + }); + + return testCafe.initializeBrowserConnectionGateway(); } - return openLocalBrowsers(); + return openRemoteBrowsers(); }) - .then(function () { + .then(() => { + return waitUntilBrowsersConnected(); + }) + .then(() => { global.testReport = null; global.testCafe = testCafe; - global.runTests = function (fixture, testName, opts) { - var report = ''; - var runner = testCafe.createRunner(); - var fixturePath = path.isAbsolute(fixture) ? fixture : path.join(path.dirname(caller()), fixture); - var skipJsErrors = opts && opts.skipJsErrors; - var quarantineMode = opts && opts.quarantineMode; - var selectorTimeout = opts && opts.selectorTimeout || FUNCTIONAL_TESTS_SELECTOR_TIMEOUT; - var assertionTimeout = opts && opts.assertionTimeout || FUNCTIONAL_TESTS_ASSERTION_TIMEOUT; - var onlyOption = opts && opts.only; - var skipOption = opts && opts.skip; - var screenshotPath = opts && opts.setScreenshotPath ? '___test-screenshots___' : ''; - var screenshotsOnFails = opts && opts.screenshotsOnFails; - var speed = opts && opts.speed; - var appCommand = opts && opts.appCommand; - var appInitDelay = opts && opts.appInitDelay; - var externalProxyHost = opts && opts.useProxy; - - var actualBrowsers = browsersInfo.filter(function (browserInfo) { - var only = onlyOption ? onlyOption.indexOf(browserInfo.settings.alias) > -1 : true; - var skip = skipOption ? skipOption.indexOf(browserInfo.settings.alias) > -1 : false; + global.runTests = (fixture, testName, opts = {}) => { + const stream = createSimpleTestStream(); + const runner = testCafe.createRunner(); + const fixturePath = typeof fixture !== 'string' || + path.isAbsolute(fixture) ? fixture : path.join(path.dirname(caller()), fixture); + const selectorTimeout = opts && opts.selectorTimeout || FUNCTIONAL_TESTS_SELECTOR_TIMEOUT; + const assertionTimeout = opts && opts.assertionTimeout || FUNCTIONAL_TESTS_ASSERTION_TIMEOUT; + const pageLoadTimeout = opts && opts.pageLoadTimeout || FUNCTIONAL_TESTS_PAGE_LOAD_TIMEOUT; + const screenshotPath = opts && opts.setScreenshotPath ? config.testScreenshotsDir : ''; + const videoPath = opts && opts.setVideoPath ? config.testVideosDir : ''; + const clientScripts = opts && opts.clientScripts || []; + const compilerOptions = opts && opts.compilerOptions; + + testCafe.runner = runner; + + const { + skipJsErrors, + quarantineMode, + screenshotPathPattern, + screenshotPathPatternOnFails, + screenshotsOnFails, + screenshotsFullPage, + videoOptions, + videoEncodingOptions, + speed, + appCommand, + appInitDelay, + proxyBypass, + skipUncaughtErrors, + reporter: customReporters, + useProxy: proxy, + only: onlyOption, + skip: skipOption, + stopOnFirstFail, + disablePageCaching, + disablePageReloads, + disableScreenshots, + disableMultipleWindows, + experimentalMultipleWindows, + pageRequestTimeout, + ajaxRequestTimeout, + userVariables, + tsConfigPath, + hooks, + testExecutionTimeout, + runExecutionTimeout, + baseUrl, + customActions, + } = opts; + + const actualBrowsers = browsersInfo.filter(browserInfo => { + const { alias, userAgent } = browserInfo.settings; + + const only = onlyOption ? [alias, userAgent].some(prop => onlyOption.includes(prop)) : true; + const skip = skipOption ? [alias, userAgent].some(prop => skipOption.includes(prop)) : false; return only && !skip; }); if (!actualBrowsers.length) { - mocha.test.skip(); + global.currentTest.skip(); + return Promise.resolve(); } - var connections = actualBrowsers.map(function (browserInfo) { + const connections = actualBrowsers.map(browserInfo => { return browserInfo.connection; }); - var handleError = function (err) { - var shouldFail = opts && opts.shouldFail; + const handleError = (err) => { + const shouldFail = opts && opts.shouldFail; if (shouldFail && !err) throw new Error('Test should have failed but it succeeded'); @@ -190,39 +328,70 @@ before(function () { throw err; }; + if (customReporters) + runner.reporter(customReporters); + else + runner.reporter('json', stream); + + if (config.nativeAutomation) + setNativeAutomationForRemoteConnection(runner); + return runner - .useProxy(externalProxyHost) + .useProxy(proxy, proxyBypass) .browsers(connections) - .filter(function (test) { + + .filter(test => { return testName ? test === testName : true; }) - .reporter('json', { - write: function (data) { - report += data; - }, - - end: function (data) { - report += data; - } - }) .src(fixturePath) - .screenshots(screenshotPath, screenshotsOnFails) + .screenshots({ + path: screenshotPath, + takeOnFails: screenshotsOnFails, + pathPattern: screenshotPathPattern, + pathPatternOnFails: screenshotPathPatternOnFails, + fullPage: screenshotsFullPage, + }) + .video(videoPath, videoOptions, videoEncodingOptions) .startApp(appCommand, appInitDelay) + .clientScripts(clientScripts) + .compilerOptions(compilerOptions) .run({ - skipJsErrors: skipJsErrors, - quarantineMode: quarantineMode, - selectorTimeout: selectorTimeout, - assertionTimeout: assertionTimeout, - speed: speed + skipJsErrors, + quarantineMode, + selectorTimeout, + assertionTimeout, + pageLoadTimeout, + speed, + stopOnFirstFail, + skipUncaughtErrors, + disablePageCaching, + disablePageReloads, + disableScreenshots, + disableMultipleWindows, + experimentalMultipleWindows, + pageRequestTimeout, + ajaxRequestTimeout, + userVariables, + tsConfigPath, + hooks, + testExecutionTimeout, + runExecutionTimeout, + baseUrl, + customActions, + nativeAutomation: config.nativeAutomation, }) - .then(function () { - var taskReport = JSON.parse(report); - var errorDescr = getTestError(taskReport, actualBrowsers); - var testReport = taskReport.fixtures.length === 1 ? - taskReport.fixtures[0].tests[0] : - taskReport; + .then(failedCount => { + if (customReporters) + return; + + const taskReport = JSON.parse(stream.data); + const errorDescr = getTestError(taskReport, actualBrowsers); + const testReport = taskReport.fixtures.length === 1 ? + taskReport.fixtures[0].tests[0] : + taskReport; - testReport.warnings = taskReport.warnings; + testReport.warnings = taskReport.warnings; + testReport.failedCount = failedCount; global.testReport = testReport; @@ -233,7 +402,11 @@ before(function () { }); }); -after(function () { +beforeEach(function () { + global.currentTest = this.currentTest; +}); + +after(async function () { this.timeout(60000); testCafe.close(); @@ -243,8 +416,15 @@ after(function () { delete global.runTests; delete global.testReport; - if (!config.useLocalBrowsers) - return closeRemoteBrowsers(); - - return closeLocalBrowsers(); + if (!USE_PROVIDER_POOL) { + // TODO: we should determine the reason why Browserstack browser hangs at the end + // HACK: the timeout prevents tests from failing when we can't close Browserstack browsers + await Promise.race([ + closeRemoteBrowsers(), + new Promise(resolve => setTimeout(resolve, 57000)), + ]); + } + else + await closeLocalBrowsers(); }); + diff --git a/test/functional/site/api-redirect.js b/test/functional/site/api-redirect.js new file mode 100644 index 00000000000..6ab2714261b --- /dev/null +++ b/test/functional/site/api-redirect.js @@ -0,0 +1,20 @@ +const router = require('express').Router(); + +const responses = { + handleGetResult: (res) => { + return { + data: { + name: 'James Livers', + position: 'CEO', + }, + params: res.query, + cookies: res.headers.cookie, + }; + }, +}; + +router.get('/data', (req, res) => { + res.send(responses.handleGetResult(req)); +}); + +module.exports = router; diff --git a/test/functional/site/api.js b/test/functional/site/api.js new file mode 100644 index 00000000000..5b103574d99 --- /dev/null +++ b/test/functional/site/api.js @@ -0,0 +1,115 @@ +const path = require('path'); +const fs = require('fs'); +const { noop } = require('lodash'); +const router = require('express').Router(); + +const responses = { + loadingResult: {}, + handleGetResult: (res) => { + return { + data: { + name: 'John Hearts', + position: 'CTO', + }, + params: res.query, + cookies: res.headers.cookie, + }; + }, + handlePostResult: (data) => ({ + message: 'Data was posted', + data, + }), + handleDeleteResult: (data) => ({ + message: 'Data was deleted', + data, + }), + handlePutResult: (data) => ({ + message: 'Data was putted', + data, + }), + handlePatchResult: (data) => ({ + message: 'Data was patched', + data, + }), +}; + +router.get('/data', (req, res) => { + res.send(responses.handleGetResult(req)); +}); + +router.get('/data/text', (req, res) => { + res.send(JSON.stringify(responses.handleGetResult(req).data)); +}); + +router.get('/data/file', (req, res) => { + const pathFile = path.resolve('test/file'); + + fs.writeFileSync(pathFile, JSON.stringify(responses.handleGetResult(req).data)); + res.sendFile(pathFile); + fs.rm(pathFile, { + force: true, + }, noop); +}); + +router.get('/data/loading', (req, res) => { + setTimeout(() => { + Object.assign(responses.loadingResult, responses.handleGetResult(req).data); + }, 100); + + res.send(responses.loadingResult); +}); + +router.get('/hanging', () => { }); + +router.get('/cookies', (req, res) => { + res.cookie('cookieName', 'cookieValue'); + res.send(); +}); + +router.post('/auth/basic', (req, res) => { + res.send({ + token: req.rawHeaders[req.rawHeaders.indexOf('authorization') + 1], + }); +}); + +router.post('/auth/proxy/basic', (req, res) => { + res.send({ + token: req.rawHeaders[req.rawHeaders.indexOf('proxy-authorization') + 1], + }); +}); + +router.post('/auth/bearer', (req, res) => { + res.send(req.rawHeaders[req.rawHeaders.indexOf('authorization') + 1] ? 'authorized' : 'un-authorized'); +}); + +router.post('/auth/key', (req, res) => { + res.send(req.rawHeaders[req.rawHeaders.indexOf('API-KEY') + 1] ? 'authorized' : 'un-authorized'); +}); + +router.post('/data', (req, res) => { + res.send(responses.handlePostResult(req.body)); +}); + +router.post('/request-info', (req, res) => { + res.send({ + headers: req.headers, + }); +}); + +router.delete('/data/:dataId', (req, res) => { + res.send(responses.handleDeleteResult(req.params)); +}); + +router.put('/data', (req, res) => { + res.send(responses.handlePutResult(req.body)); +}); + +router.patch('/data', (req, res) => { + res.send(responses.handlePatchResult(req.body)); +}); + +router.head('/data', (req, res) => { + res.send(); +}); + +module.exports = router; diff --git a/test/functional/site/basic-auth-server.js b/test/functional/site/basic-auth-server.js index 084edd21221..0bb9a31c142 100644 --- a/test/functional/site/basic-auth-server.js +++ b/test/functional/site/basic-auth-server.js @@ -1,48 +1,42 @@ -var http = require('http'); -var express = require('express'); -var basicAuth = require('basic-auth'); - - -var server = null; -var sockets = null; - -function start (port) { - var app = express(); - - app.all('*', function (req, res) { - var credentials = basicAuth(req); - - if (!credentials || credentials.name !== 'username' || credentials.pass !== 'password') { - res.statusCode = 401; - res.setHeader('WWW-Authenticate', 'Basic realm="example"'); - res.end('
not authorized
'); - } - else { - res.statusCode = 200; - res.end('
authorized
'); - } - }); - - server = http.createServer(app).listen(port); - sockets = []; - - var connectionHandler = function (socket) { - sockets.push(socket); - - socket.on('close', function () { - sockets.splice(sockets.indexOf(socket), 1); +const http = require('http'); +const express = require('express'); +const basicAuth = require('basic-auth'); +const BasicHttpServer = require('./basic-http-server'); + +class BasicAuthServer extends BasicHttpServer { + start (port) { + const app = express(); + + app.all('/redirect', function (req, res) { + const credentials = basicAuth(req); + + if (!credentials || credentials.name !== 'username' || credentials.pass !== 'password') { + res.statusCode = 401; + res.setHeader('WWW-Authenticate', 'Basic realm="example"'); + res.end('
not authorized
'); + } + else + res.redirect('/'); }); - }; - server.on('connection', connectionHandler); -} + app.all('*', function (req, res) { + const credentials = basicAuth(req); + + if (!credentials || credentials.name !== 'username' || credentials.pass !== 'password') { + res.statusCode = 401; + res.setHeader('WWW-Authenticate', 'Basic realm="example"'); + res.end('
not authorized
'); + } + else { + res.statusCode = 200; + res.end('
authorized
'); + } + }); -function shutdown () { - server.close(); + this.server = http.createServer(app).listen(port); - sockets.forEach(socket => { - socket.destroy(); - }); + super.start(); + } } -module.exports = { start: start, shutdown: shutdown }; +module.exports = new BasicAuthServer(); diff --git a/test/functional/site/basic-http-server.js b/test/functional/site/basic-http-server.js new file mode 100644 index 00000000000..617daa8cec6 --- /dev/null +++ b/test/functional/site/basic-http-server.js @@ -0,0 +1,35 @@ +class BasicHttpServer { + constructor () { + this.server = null; + this.sockets = []; + } + + start () { + if (!this.server) + return; + + const self = this; + + this.server.on('connection', (socket) => { + self.sockets.push(socket); + + socket.on('close', function () { + self.sockets.splice(self.sockets.indexOf(socket), 1); + }); + }); + } + + shutdown () { + if (!this.server) + return; + + this.server.close(); + + this.sockets.forEach(socket => { + socket.destroy(); + }); + } +} + +module.exports = BasicHttpServer; + diff --git a/test/functional/site/index.js b/test/functional/site/index.js index 2dea9e1ae3c..7b4636e9707 100644 --- a/test/functional/site/index.js +++ b/test/functional/site/index.js @@ -1,28 +1,34 @@ -var Server = require('./server'); -var basicAuthServer = require('./basic-auth-server'); -var ntlmAuthServer = require('./ntlm-auth-server'); -var trustedProxyServer = require('./trusted-proxy-server'); -var transparentProxyServer = require('./transparent-proxy-server'); +const Server = require('./server'); +const basicAuthServer = require('./basic-auth-server'); +const ntlmAuthServer = require('./ntlm-auth-server'); +const trustedProxyServer = require('./trusted-proxy-server'); +const transparentProxyServer = require('./transparent-proxy-server'); +const invalidCertificateHttpsServer = require('./invalid-certificate-https-server'); +const apiRouter = require('./api'); +const apiRedirectRouter = require('./api-redirect'); -var server1 = null; -var server2 = null; +let server1 = null; +let server2 = null; exports.create = function (ports, viewsPath) { - server1 = new Server(ports.server1, viewsPath); - server2 = new Server(ports.server2, viewsPath); + server1 = new Server(ports.server1, viewsPath, apiRouter); + server2 = new Server(ports.server2, viewsPath, apiRedirectRouter); basicAuthServer.start(ports.basicAuthServer); ntlmAuthServer.start(ports.ntlmAuthServer); trustedProxyServer.start(ports.trustedProxyServer); transparentProxyServer.start(ports.transparentProxyServer); + invalidCertificateHttpsServer.start(ports.invalidCertificateHttpsServer); }; exports.destroy = function () { server1.close(); server2.close(); + basicAuthServer.shutdown(); ntlmAuthServer.shutdown(); trustedProxyServer.shutdown(); transparentProxyServer.shutdown(); + invalidCertificateHttpsServer.shutdown(); }; diff --git a/test/functional/site/invalid-certificate-https-server.js b/test/functional/site/invalid-certificate-https-server.js new file mode 100644 index 00000000000..2da8bbd917f --- /dev/null +++ b/test/functional/site/invalid-certificate-https-server.js @@ -0,0 +1,30 @@ +const https = require('https'); +const selfSignedCertificate = require('openssl-self-signed-certificate'); +const BasicHttpServer = require('./basic-http-server'); +const express = require('express'); + +// NOTE: browser interprets the self-signed certificate as invalid. +class InvalidCertificateHttpsServer extends BasicHttpServer { + start (port) { + const app = express(); + + app.get('/data', (req, res) => { + res.header('content-type', 'application/json; charset=utf-8'); + res.json({ + name: 'John Hearts', + position: 'CTO', + }); + }); + + app.all('*', (req, res) => { + res.status(200); + res.send('Page>'); + }); + + this.server = https.createServer(selfSignedCertificate, app).listen(port); + + super.start(); + } +} + +module.exports = new InvalidCertificateHttpsServer(); diff --git a/test/functional/site/ntlm-auth-server.js b/test/functional/site/ntlm-auth-server.js index 158f92adaae..4cb281daa3f 100644 --- a/test/functional/site/ntlm-auth-server.js +++ b/test/functional/site/ntlm-auth-server.js @@ -1,39 +1,22 @@ -var http = require('http'); -var express = require('express'); -var ntlm = require('express-ntlm'); +const http = require('http'); +const express = require('express'); +const ntlm = require('express-ntlm'); +const BasicHttpServer = require('./basic-http-server'); -var server = null; -var sockets = null; +class NtlmAuthServer extends BasicHttpServer { + start (port) { + const app = express(); -function start (port) { - var app = express(); + app.use(ntlm()); - app.use(ntlm()); - - app.all('*', function (req, res) { - res.end('
' + JSON.stringify(req.ntlm) + '
'); - }); - - server = http.createServer(app).listen(port); - sockets = []; - - var connectionHandler = function (socket) { - sockets.push(socket); - - socket.on('close', function () { - sockets.splice(sockets.indexOf(socket), 1); + app.all('*', function (req, res) { + res.end('
' + JSON.stringify(req.ntlm) + '
'); }); - }; - - server.on('connection', connectionHandler); -} -function shutdown () { - server.close(); + this.server = http.createServer(app).listen(port); - sockets.forEach(socket => { - socket.destroy(); - }); + super.start(); + } } -module.exports = { start: start, shutdown: shutdown }; +module.exports = new NtlmAuthServer(); diff --git a/test/functional/site/server.js b/test/functional/site/server.js index 3cae04b3e95..ca0b83504ad 100644 --- a/test/functional/site/server.js +++ b/test/functional/site/server.js @@ -1,37 +1,56 @@ -var express = require('express'); -var http = require('http'); -var path = require('path'); -var bodyParser = require('body-parser'); -var readSync = require('read-file-relative').readSync; -var multer = require('multer'); -var Mustache = require('mustache'); -var readFile = require('../../../lib/utils/promisified-functions').readFile; -var quarantineModeTracker = require('../quarantine-mode-tracker'); - -var storage = multer.memoryStorage(); -var upload = multer({ storage: storage }); - -var CONTENT_TYPES = { +const express = require('express'); +const http = require('http'); +const path = require('path'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const { readSync } = require('read-file-relative'); +const multer = require('multer'); +const Mustache = require('mustache'); +const { readFile } = require('../../../lib/utils/promisified-functions'); +const quarantineModeTracker = require('../quarantine-mode-tracker'); +const { parseUserAgent } = require('../../../lib/utils/parse-user-agent'); + +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +const CONTENT_TYPES = { '.js': 'application/javascript', '.css': 'text/css', '.html': 'text/html', - '.png': 'image/png' + '.png': 'image/png', + '.zip': 'application/zip', + '.pdf': 'application/pdf', + '.xml': 'application/xml', }; -var UPLOAD_SUCCESS_PAGE_TEMPLATE = readSync('./views/upload-success.html.mustache'); +const NON_CACHEABLE_PAGES = [ + '/fixtures/api/es-next/roles/pages', + '/fixtures/api/es-next/request-hooks/pages', + '/fixtures/regression/gh-2015/pages', + '/fixtures/regression/gh-2282/pages', +]; + +const UPLOAD_SUCCESS_PAGE_TEMPLATE = readSync('./views/upload-success.html.mustache'); +const shouldCachePage = function (reqUrl) { + return NON_CACHEABLE_PAGES.every(pagePrefix => !reqUrl.startsWith(pagePrefix)); +}; -var Server = module.exports = function (port, basePath) { - var server = this; +const Server = module.exports = function (port, basePath, apiRouter) { + const server = this; this.app = express().use(bodyParser.urlencoded({ extended: false })); this.appServer = http.createServer(this.app).listen(port); this.sockets = []; this.basePath = basePath; - this._setupRoutes(); + this.app.use(cors()); + + this.app.use(bodyParser.json()); + + this._setupRoutes(apiRouter); - var handler = function (socket) { + const handler = function (socket) { server.sockets.push(socket); socket.on('close', function () { server.sockets.splice(server.sockets.indexOf(socket), 1); @@ -41,24 +60,127 @@ var Server = module.exports = function (port, basePath) { this.appServer.on('connection', handler); }; -Server.prototype._setupRoutes = function () { - var server = this; +Server.prototype._setupRoutes = function (apiRouter) { + const server = this; + + this.app.use('/api', apiRouter); this.app.get('/download', function (req, res) { - var filePath = path.join(server.basePath, '../../package.json'); + const filePath = path.join(server.basePath, '../../package.json'); res.download(filePath); }); + this.app.get('/get-browser-name', function (req, res) { + const parsedUA = parseUserAgent(req.headers['user-agent']); + + res.end(parsedUA.name); + }); + + this.app.get('/trim-bom', (req, res) => { + res.send(`${String.fromCharCode(65279)}`); + }); + + this.app.get('/i4855', (req, res) => { + res.send(` + + + + + + `); + }); + + this.app.get('/redirect', (req, res) => { + res.redirect(req.query.page); + }); + + this.app.get('/fixtures/request-pipeline/content-security-policy/pages/csp.html', (req, res, next) => { + res.setHeader('Content-Security-Policy', 'script-src \'self\''); + + next(); + }); + + this.app.get('/204', function (req, res) { + res.status(204); + res.end(); + }); + + this.app.get('/fixtures/regression/gh-7874/', (req, res) => { + res.send(` + + + + + GH-7874 + + + + + + + `); + }); + + this.app.get('/fixtures/regression/gh-7529/', function (req, res) { + const html = ` + + + + + GH-7529 + + +

codage réussi

+ + + `; + + const content = Buffer.from(html, 'latin1'); + + res.setHeader('content-type', 'text/html; charset=iso-8859-15'); + res.send(content); + }); + this.app.get('*', function (req, res) { - var reqPath = req.params[0] || ''; - var resourcePath = path.join(server.basePath, reqPath); - var delay = req.query.delay ? parseInt(req.query.delay, 10) : 0; + const reqPath = req.params[0] || ''; + const resourcePath = path.join(server.basePath, reqPath); + const delay = req.query.delay ? parseInt(req.query.delay, 10) : 0; readFile(resourcePath) .then(function (content) { res.setHeader('content-type', CONTENT_TYPES[path.extname(resourcePath)]); + if (shouldCachePage(reqPath)) + res.setHeader('cache-control', 'max-age=3600'); + setTimeout(function () { res.send(content); }, delay); @@ -87,21 +209,64 @@ Server.prototype._setupRoutes = function () { res.redirect(req.headers['referer']); }); + this.app.post('/set-token-and-close', (req, res) => { + res.setHeader('set-cookie', 'token=' + req.body.token); + res.send(` + + + + + + `); + }); + this.app.post('/file-upload', upload.any(), function (req, res) { - var filesData = req.files.map(function (file) { + const filesData = req.files.map(function (file) { return file.buffer.toString(); }); res.end(Mustache.render(UPLOAD_SUCCESS_PAGE_TEMPLATE, { uploadedDataArray: filesData })); }); + this.app.post('/file-upload-size', upload.any(), function (req, res) { + const filesData = req.files.map(function (file) { + return file.size; + }); + + res.end(`${filesData[0]}`); + }); + + this.app.post('/xhr/test-header', function (req, res) { + res.send(req.headers.test); + }); + + this.app.post('/xhr/auth-header', function (req, res) { + res.setHeader('authorization', 'authorization-string'); + res.send(); + }); + this.app.post('/xhr/:delay', function (req, res) { - var delay = req.params.delay || 0; + const delay = req.params.delay || 0; setTimeout(function () { res.send(delay.toString()); }, delay); }); + + this.app.post('/echo-custom-request-headers-in-response-headers', (req, res) => { + Object.keys(req.headers).forEach(headerName => { + if (headerName.startsWith('x-header-')) + res.setHeader(headerName, req.headers[headerName]); + }); + + res.end(); + }); + + this.app.options('/options', function (req, res) { + res.send(); + }); }; Server.prototype.close = function () { diff --git a/test/functional/site/transparent-proxy-server.js b/test/functional/site/transparent-proxy-server.js index b930ef5a9b0..4576229d35b 100644 --- a/test/functional/site/transparent-proxy-server.js +++ b/test/functional/site/transparent-proxy-server.js @@ -1,59 +1,50 @@ -var http = require('http'); -var urlLib = require('url'); +const http = require('http'); +const urlLib = require('url'); +const BasicHttpServer = require('./basic-http-server'); -var server = null; -var sockets = null; +class TransparentProxyServer extends BasicHttpServer { + constructor () { + super(); -var agentsCache = {}; + this.agentsCache = {}; + } -function start (port) { - sockets = []; + _getUserAgent (reqOptions) { + return reqOptions.headers['user-agent']; + } - server = http - .createServer() - .listen(port); + start (port) { + this.server = http.createServer().listen(port); - server - .on('request', (req, res) => { - var reqOptions = urlLib.parse(req.url); + this.server + .on('request', (req, res) => { + const reqOptions = urlLib.parse(req.url); + const self = this; - reqOptions.method = req.method; - reqOptions.headers = req.headers; + reqOptions.method = req.method; + reqOptions.headers = req.headers; - if (!agentsCache[reqOptions.headers['user-agent']]) - agentsCache[reqOptions.headers['user-agent']] = new http.Agent({ keepAlive: true }); + const userAgent = this._getUserAgent(reqOptions); - reqOptions.agent = agentsCache[reqOptions.headers['user-agent']]; + if (!this.agentsCache[userAgent]) + this.agentsCache[userAgent] = new http.Agent({ keepAlive: true }); - var serverReq = http.request(reqOptions, function (serverRes) { - res.writeHead(serverRes.statusCode, serverRes.headers); + reqOptions.agent = this.agentsCache[userAgent]; - if (serverRes.headers.connection && serverRes.headers.connection === 'close') - delete agentsCache[reqOptions.headers['user-agent']]; + const serverReq = http.request(reqOptions, function (serverRes) { + res.writeHead(serverRes.statusCode, serverRes.headers); - serverRes.pipe(res); - }); - - req.pipe(serverReq); - }); - - var connectionHandler = function (socket) { - sockets.push(socket); + if (serverRes.headers.connection && serverRes.headers.connection === 'close') + delete self.agentsCache[self._getUserAgent(reqOptions)]; - socket.on('close', function () { - sockets.splice(sockets.indexOf(socket), 1); - }); - }; + serverRes.pipe(res); + }); - server.on('connection', connectionHandler); -} - -function shutdown () { - server.close(); + req.pipe(serverReq); + }); - sockets.forEach(socket => { - socket.destroy(); - }); + super.start(); + } } -module.exports = { start: start, shutdown: shutdown }; +module.exports = new TransparentProxyServer(); diff --git a/test/functional/site/trusted-proxy-server.js b/test/functional/site/trusted-proxy-server.js index 37e450d09fb..bc6cbf9c467 100644 --- a/test/functional/site/trusted-proxy-server.js +++ b/test/functional/site/trusted-proxy-server.js @@ -1,47 +1,27 @@ -var http = require('http'); -var urlLib = require('url'); +const http = require('http'); +const urlLib = require('url'); +const BasicHttpServer = require('./basic-http-server'); -var server = null; -var sockets = null; +class TrustedProxyServer extends BasicHttpServer { + start (port) { + this.server = http.createServer().listen(port); -function start (port) { - server = http - .createServer() - .listen(port); + this.server.on('request', (req, res) => { + const reqOptions = urlLib.parse(req.url); - sockets = []; + reqOptions.method = req.method; + reqOptions.auth = 'username:password'; - server.on('request', (req, res) => { - var reqOptions = urlLib.parse(req.url); + const serverReq = http.request(reqOptions, function (serverRes) { + res.writeHead(serverRes.statusCode, serverRes.headers); + serverRes.pipe(res); + }); - reqOptions.method = req.method; - reqOptions.auth = 'username:password'; - - var serverReq = http.request(reqOptions, function (serverRes) { - res.writeHead(serverRes.statusCode, serverRes.headers); - serverRes.pipe(res); - }); - - req.pipe(serverReq); - }); - - var connectionHandler = function (socket) { - sockets.push(socket); - - socket.on('close', function () { - sockets.splice(sockets.indexOf(socket), 1); + req.pipe(serverReq); }); - }; - - server.on('connection', connectionHandler); -} - -function shutdown () { - server.close(); - sockets.forEach(socket => { - socket.destroy(); - }); + super.start(); + } } -module.exports = { start: start, shutdown: shutdown }; +module.exports = new TrustedProxyServer(); diff --git a/test/functional/utils/file-storage.js b/test/functional/utils/file-storage.js new file mode 100644 index 00000000000..5e59af125c1 --- /dev/null +++ b/test/functional/utils/file-storage.js @@ -0,0 +1,54 @@ +const fs = require('fs'); +const path = require('path'); + +class FileStorage { + constructor (fileName, dirName) { + this.fileName = fileName; + this.fullPath = path.resolve(dirName, fileName); + this.data = []; + } + + load () { + this.data = this.getData(); + } + + add (val) { + this.data.push(val); + } + + setData (val) { + this.data = val; + } + + getData () { + try { + const dataStr = fs.readFileSync(this.fullPath).toString(); + + return JSON.parse(dataStr); + } + catch (err) { + return []; + } + } + + clear () { + this.data = []; + } + + delete () { + if (fs.existsSync(this.fullPath)) + fs.unlinkSync(this.fullPath); + } + + save () { + fs.writeFileSync(this.fullPath, JSON.stringify(this.data)); + } + + safeAdd (val) { + this.load(); + this.add(val); + this.save(); + } +} + +module.exports = FileStorage; diff --git a/test/functional/utils/reporter.js b/test/functional/utils/reporter.js new file mode 100644 index 00000000000..3a5a2263d90 --- /dev/null +++ b/test/functional/utils/reporter.js @@ -0,0 +1,10 @@ +const { noop } = require('lodash'); + +module.exports.createReporter = (reporterInit) => { + return () => Object.assign({ + reportTaskStart: noop, + reportTestDone: noop, + reportFixtureStart: noop, + reportTaskDone: noop, + }, reporterInit); +}; diff --git a/test/functional/utils/run-in-cli.js b/test/functional/utils/run-in-cli.js new file mode 100644 index 00000000000..90392598314 --- /dev/null +++ b/test/functional/utils/run-in-cli.js @@ -0,0 +1,14 @@ +const path = require('path'); +const { exec } = require('child_process'); + +module.exports = function runInCLI ({ testFile, browsers, args = [] }) { + const testcafePath = path.resolve('bin/testcafe'); + const testFilePath = path.resolve(testFile); + const command = `node ${testcafePath} ${browsers} ${testFilePath} ${args.join(' ')}`; + + return new Promise(resolve => { + exec(command, (error, stdout) => { + resolve({ error, stdout }); + }); + }); +}; diff --git a/test/functional/utils/run-tests-with-config.js b/test/functional/utils/run-tests-with-config.js new file mode 100644 index 00000000000..a36122749a9 --- /dev/null +++ b/test/functional/utils/run-tests-with-config.js @@ -0,0 +1,16 @@ +const path = require('path'); +const createTestCafe = require('../../../lib'); +const config = require('../config'); + +module.exports = async (testName, configPath) => { + if (!configPath) + throw new Error('"configPath" isn\'t defined'); + + const cafe = await createTestCafe({ configFile: path.resolve(configPath) }); + const runner = cafe.createRunner(); + const failedCount = await runner.run({ disableNativeAutomation: !config.nativeAutomation }); + + await cafe.close(); + + return failedCount; +}; diff --git a/test/functional/utils/set-native-automation-for-remote-connection.js b/test/functional/utils/set-native-automation-for-remote-connection.js new file mode 100644 index 00000000000..ae645d16c91 --- /dev/null +++ b/test/functional/utils/set-native-automation-for-remote-connection.js @@ -0,0 +1,6 @@ +const { noop } = require('lodash'); + +module.exports = function setNativeAutomationForRemoteConnection (runner) { + // Hack: it's necessary to run remote browsers in the native automation mode. + runner.bootstrapper._disableNativeAutomationIfNecessary = noop; +}; diff --git a/test/functional/utils/skip-in.js b/test/functional/utils/skip-in.js new file mode 100644 index 00000000000..3e857270bba --- /dev/null +++ b/test/functional/utils/skip-in.js @@ -0,0 +1,14 @@ +const config = require('../config'); + +const skipInNativeAutomation = config.nativeAutomation ? it.skip : it; +const skipDescribeInNativeAutomation = config.nativeAutomation ? describe.skip : describe; +const onlyInNativeAutomation = config.nativeAutomation ? it : it.skip; +const onlyDescribeInNativeAutomation = config.nativeAutomation ? describe : describe.skip; + +module.exports = { + skipInNativeAutomation, + skipDescribeInNativeAutomation, + onlyInNativeAutomation, + onlyDescribeInNativeAutomation, +}; + diff --git a/test/functional/utils/stream.js b/test/functional/utils/stream.js new file mode 100644 index 00000000000..eb2b8b6935b --- /dev/null +++ b/test/functional/utils/stream.js @@ -0,0 +1,76 @@ +const { Writable: WritableStream } = require('stream'); + +const ASYNC_REPORTER_FINALIZING_TIMEOUT = 2000; + +module.exports.createSimpleTestStream = () => { + return { + data: '', + + writable: true, + + pipe: () => {}, + + _write (val) { + this.data += val; + }, + + _writableState: {}, + + write (val) { + this._write(val); + }, + end (val) { + if (val === void 0) + return; + + this._write(val); + }, + }; +}; + +module.exports.createAsyncTestStream = ({ shouldFail } = {}) => { + return new WritableStream({ + write (chunk, enc, cb) { + cb(); + }, + + final (cb) { + setTimeout(() => { + this.finalCalled = true; + + cb(shouldFail ? new Error('Stream failed') : null); + }, ASYNC_REPORTER_FINALIZING_TIMEOUT); + }, + }); +}; + +module.exports.createSyncTestStream = () => { + const SyncTestStream = class extends WritableStream { + _callLastArg (args) { + const lastArg = args && args.pop(); + + if (typeof lastArg === 'function') + lastArg(); + } + + write (...args) { + this._callLastArg(args); + } + + end (...args) { + this.finalCalled = true; + + this.emit('finish'); + this._callLastArg(args); + } + }; + + return new SyncTestStream(); +}; + +module.exports.createNullStream = () => { + return { + write: () => {}, + end: () => {}, + }; +}; diff --git a/test/functional/utils/warning-reporter.js b/test/functional/utils/warning-reporter.js new file mode 100644 index 00000000000..940a0742aea --- /dev/null +++ b/test/functional/utils/warning-reporter.js @@ -0,0 +1,38 @@ +const { createReporter } = require('./reporter'); +const { expect } = require('chai'); + +module.exports.createWarningReporter = () => { + const warningResult = { + warnings: [], + actionStartList: {}, + actionDoneList: {}, + }; + + const reporter = createReporter({ + reportWarnings: warning => { + warningResult.warnings.push(warning); + }, + reportTestActionStart: (name, { command }) => { + warningResult.actionStartList[command.actionId] = name; + + }, + reportTestActionDone: (name, { command }) => { + warningResult.actionDoneList[command.actionId] = name; + }, + }); + + function assertReporterWarnings (actionName) { + expect(warningResult.warnings.length).gte(1); + + for (const warning of warningResult.warnings) { + expect(warningResult.actionStartList[warning.actionId]).eql(actionName); + expect(warningResult.actionDoneList[warning.actionId]).eql(actionName); + } + } + + return { + warningResult, + assertReporterWarnings, + reporter, + }; +}; diff --git a/test/server/.eslintrc b/test/server/.eslintrc index bff91ca29e7..9baca0b5064 100644 --- a/test/server/.eslintrc +++ b/test/server/.eslintrc @@ -1,7 +1,11 @@ { + "plugins": [ + "no-only-tests" + ], "rules": { "no-unused-expressions": 0, - "max-nested-callbacks": [2, 5] + "max-nested-callbacks": [2, 6], + "no-only-tests/no-only-tests": 2 }, "env": { "mocha": true diff --git a/test/server/api-test.js b/test/server/api-test.js index fe4afe63430..6c71d982a03 100644 --- a/test/server/api-test.js +++ b/test/server/api-test.js @@ -1,7 +1,14 @@ -var expect = require('chai').expect; -var resolve = require('path').resolve; -var assertAPIError = require('./helpers/assert-error').assertAPIError; -var compile = require('./helpers/compile'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const sinon = require('sinon'); +const { resolve } = require('path'); +const { assertAPIError } = require('./helpers/assert-runtime-error'); +const compile = require('./helpers/compile'); +const OPTION_NAMES = require('../../lib/configuration/option-names'); +const Compiler = require('../../lib/compiler'); +const { RUNTIME_ERRORS } = require('../../lib/errors/types'); +const Fixture = require('../../lib/api/structure/fixture'); + describe('API', function () { this.timeout(20000); @@ -16,7 +23,7 @@ describe('API', function () { }); it('Should raise an error if fixture name is not a string', function () { - var testfile = resolve('test/server/data/test-suites/fixture-name-is-not-a-string/testfile.js'); + const testfile = resolve('test/server/data/test-suites/fixture-name-is-not-a-string/testfile.js'); return compile(testfile) .then(function () { @@ -26,8 +33,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'The fixture name is expected to be a string, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The fixture name (object) is not of expected type (string).', callsite: ' 2 |// (to treat a file as a test, it requires at least one fixture definition\n' + ' 3 |// with the string argument).\n' + @@ -39,13 +46,13 @@ describe('API', function () { " 9 |test('Test', () => {\n" + " 10 | return 'yo';\n" + ' 11 |});\n' + - ' 12 |' + ' 12 |', }); }); }); it('Should raise an error if fixture page is not a string', function () { - var testfile = resolve('test/server/data/test-suites/fixture-page-is-not-a-string/testfile.js'); + const testfile = resolve('test/server/data/test-suites/fixture-page-is-not-a-string/testfile.js'); return compile(testfile) .then(function () { @@ -55,8 +62,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'The page URL is expected to be a string, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The page URL (object) is not of expected type (string).', callsite: ' 1 |fixture `Yo`\n' + ' > 2 | .page({ answer: 42 });\n' + @@ -64,13 +71,13 @@ describe('API', function () { " 4 |test('Test', () => {\n" + " 5 | return 'yo';\n" + ' 6 |});\n' + - ' 7 |' + ' 7 |', }); }); }); it('Should raise an error if beforeEach is not a function', function () { - var testfile = resolve('test/server/data/test-suites/before-each-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/before-each-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -80,8 +87,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'fixture.beforeEach hook is expected to be a function, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The fixture.beforeEach hook (string) is not of expected type (function).', callsite: ' 1 |fixture `beforeEach is not a function`\n' + " > 2 | .beforeEach('yo');\n" + @@ -89,13 +96,13 @@ describe('API', function () { " 4 |test('Some test', () => {\n" + ' 5 |\n' + ' 6 |});\n' + - ' 7 |' + ' 7 |', }); }); }); it('Should raise an error if afterEach is not a function', function () { - var testfile = resolve('test/server/data/test-suites/after-each-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/after-each-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -105,8 +112,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'fixture.afterEach hook is expected to be a function, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The fixture.afterEach hook (string) is not of expected type (function).', callsite: ' 1 |fixture `afterEach is not a function`\n' + " > 2 | .afterEach('yo');\n" + @@ -114,13 +121,13 @@ describe('API', function () { " 4 |test('Some test', () => {\n" + ' 5 |\n' + ' 6 |});\n' + - ' 7 |' + ' 7 |', }); }); }); it('Should raise an error if fixture.before is not a function', function () { - var testfile = resolve('test/server/data/test-suites/fixture-before-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/fixture-before-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -130,8 +137,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'fixture.before hook is expected to be a function, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The fixture.before hook (string) is not of expected type (function).', callsite: ' 1 |fixture `before is not a function`\n' + " > 2 | .before('yo');\n" + @@ -139,14 +146,13 @@ describe('API', function () { " 4 |test('Some test', () => {\n" + ' 5 |\n' + ' 6 |});\n' + - ' 7 |' + ' 7 |', }); }); }); - it('Should raise an error if fixture.after is not a function', function () { - var testfile = resolve('test/server/data/test-suites/fixture-after-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/fixture-after-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -156,8 +162,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'fixture.after hook is expected to be a function, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The fixture.after hook (string) is not of expected type (function).', callsite: ' 1 |fixture `after is not a function`\n' + " > 2 | .after('yo');\n" + @@ -165,15 +171,15 @@ describe('API', function () { " 4 |test('Some test', () => {\n" + ' 5 |\n' + ' 6 |});\n' + - ' 7 |' + ' 7 |', }); }); }); it('Should raise an error if httpAuth takes a wrong argument', function () { - var credentialsInNotObject = resolve('test/server/data/test-suites/http-auth/credentials-is-not-an-object.js'); - var passIsNotString = resolve('test/server/data/test-suites/http-auth/password-is-not-a-string.js'); - var usernameIsNotDefined = resolve('test/server/data/test-suites/http-auth/username-is-not-defined.js'); + const credentialsInNotObject = resolve('test/server/data/test-suites/http-auth/credentials-is-not-an-object.js'); + const passIsNotString = resolve('test/server/data/test-suites/http-auth/password-is-not-a-string.js'); + const usernameIsNotDefined = resolve('test/server/data/test-suites/http-auth/username-is-not-defined.js'); return compile(credentialsInNotObject) .then(function () { @@ -183,8 +189,8 @@ describe('API', function () { assertAPIError(err, { stackTop: credentialsInNotObject, - message: 'Cannot prepare tests due to an error.\n\n' + - 'credentials is expected to be a non-null object, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The credentials (string) is not of expected type (non-null object).', callsite: ' 1 |fixture `Credentials is not an object`\n' + " > 2 | .httpAuth('');\n" + @@ -192,7 +198,7 @@ describe('API', function () { " 4 |test('Some test', () => {\n" + ' 5 |\n' + ' 6 |});\n' + - ' 7 |' + ' 7 |', }); return compile(passIsNotString); @@ -204,8 +210,8 @@ describe('API', function () { assertAPIError(err, { stackTop: passIsNotString, - message: 'Cannot prepare tests due to an error.\n\n' + - 'credentials.password is expected to be a string, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'credentials.password (object) is not of expected type (string).', callsite: ' 1 |fixture `Password is not a string`\n' + ' > 2 | .httpAuth({ username: \'username\', password: {} });\n' + @@ -213,7 +219,7 @@ describe('API', function () { " 4 |test('Some test', () => {\n" + ' 5 |\n' + ' 6 |});\n' + - ' 7 |' + ' 7 |', }); return compile(usernameIsNotDefined); @@ -225,8 +231,8 @@ describe('API', function () { assertAPIError(err, { stackTop: usernameIsNotDefined, - message: 'Cannot prepare tests due to an error.\n\n' + - 'credentials.username is expected to be a string, but it was undefined.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'credentials.username (undefined) is not of expected type (string).', callsite: ' 1 |fixture `Username is not defined`\n' + " > 2 | .httpAuth({ password: 'password' });\n" + @@ -234,7 +240,264 @@ describe('API', function () { " 4 |test('Some test', () => {\n" + ' 5 |\n' + ' 6 |});\n' + - ' 7 |' + ' 7 |', + }); + }); + }); + + it('Should raise an error if requestHooks takes a wrong argument', function () { + const fixtureHookHasWrongType = resolve('test/server/data/test-suites/request-hooks/fixture-hook-has-wrong-type.js'); + + return compile(fixtureHookHasWrongType) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: fixtureHookHasWrongType, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The hook (string) is not of expected type (RequestHook subclass).', + + callsite: ' 1 |fixture `RequestHook is undefined`\n' + + ' > 2 | .requestHooks(\'string\');\n' + + ' 3 |\n' + + ' 4 |test(\'test\', async t => {\n' + + ' 5 |});\n' + + ' 6 |', + + }); + }); + }); + + it('Should raise an error if "fixture.requestHooks" method calls several times', () => { + const testfile = resolve('test/server/data/test-suites/request-hooks/fixture-request-hooks-call-several-times.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'You cannot call the "requestHooks" method more than once. Specify an array of parameters instead.', + + callsite: ' 3 |const logger1 = new RequestLogger();\n' + + ' 4 |const logger2 = new RequestLogger();\n' + + ' 5 |\n' + + ' 6 |fixture `Fixture`\n' + + ' 7 | .requestHooks(logger1)\n' + + ' > 8 | .requestHooks(logger2);\n' + + ' 9 |\n' + + ' 10 |test(\'test\', async t => {});\n' + + ' 11 |', + }); + }); + }); + + it('Should collect meta data', function () { + return compile('test/server/data/test-suites/meta/testfile.js') + .then(function (compiled) { + expect(compiled.tests[0].fixture.meta.metaField1).eql('fixtureMetaValue1'); + expect(compiled.tests[0].fixture.meta.metaField2).eql('fixtureMetaUpdatedValue2'); + expect(compiled.tests[0].fixture.meta.metaField3).eql('fixtureMetaValue3'); + expect(compiled.tests[1].fixture.meta.emptyField).eql(void 0); + }); + }); + + it('Should raise an error if fixture.meta is undefined', function () { + const file = resolve('test/server/data/test-suites/meta/incorrect-fixture-meta.js'); + + return compile(file) + .then(function () { + throw new Error('Promise rejection expected'); + }) + .catch(function (err) { + assertAPIError(err, { + stackTop: file, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'fixture.meta (undefined) is not of expected type (string or a non-null object).', + + callsite: ' 1 |fixture(\'Fixture1\')\n' + + ' 2 | .page(\'http://example.com\')\n' + + ' > 3 | .meta();\n' + + ' 4 |\n' + + ' 5 |test\n' + + ' 6 | (\'Fixture1Test1\', async () => {\n' + + ' 7 | // do nothing\n' + + ' 8 | });', + }); + }); + }); + + it('Should raise an error if "fixture.clientScripts" method takes a wrong argument', () => { + const testfile = resolve('test/server/data/test-suites/custom-client-scripts/fixture-client-scripts-has-wrong-type.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The client script (number) is not of expected type (string or a client script initializer).', + + callsite: ' > 1 |fixture.clientScripts(8);\n' + + ' 2 |\n' + + ' 3 |test(\'test\', async t => {});\n' + + ' 4 |', + }); + }); + }); + + it('Should raise an error if "fixture.clientScripts" method calls several times', () => { + const testfile = resolve('test/server/data/test-suites/custom-client-scripts/fixture-client-scripts-call-several-times.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'You cannot call the "clientScripts" method more than once. Specify an array of parameters instead.', + + callsite: ' 1 |fixture `Fixture`\n' + + ' 2 | .clientScripts(\'script1.js\')\n' + + ' > 3 | .clientScripts(\'script2.js\');\n' + + ' 4 |\n' + + ' 5 |test(\'test\', async t => {});\n' + + ' 6 |', + }); + }); + }); + + it('Should set the page url for all fixture tests if the baseUrl is specified', () => { + const testfile = resolve('test/server/data/test-suites/fixture-without-page/testfile.js'); + + return compile(testfile, { }, { baseUrl: 'example.org' }) + .then(function (compiled) { + expect(compiled.fixtures[0].pageUrl).eql('http://example.org'); + expect(compiled.tests[0].pageUrl).eql('http://example.org'); + expect(compiled.tests[1].pageUrl).eql('http://example.org/index.html'); + }); + }); + + it('Should raise an error if baseUrl is relative', () => { + const testfile = resolve('test/server/data/test-suites/fixture-without-page/testfile.js'); + const createCompiler = () => new Compiler(testfile, {}, { baseUrl: './example.org' }); + + try { + createCompiler(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + const message = 'Cannot prepare tests due to the following error:\n\n' + + 'The value of the baseUrl argument cannot be relative: "./example.org"'; + const code = RUNTIME_ERRORS.relativeBaseUrl; + + expect(err.message).eql(message); + expect(err.code).eql(code); + } + }); + it('Should raise an error if baseUrl contains unsupported protocol', () => { + const testfile = resolve('test/server/data/test-suites/fixture-without-page/testfile.js'); + const createCompiler = () => new Compiler(testfile, {}, { baseUrl: 'mail://example.org' }); + + try { + createCompiler(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + const message = 'Cannot prepare tests due to the following error:\n\n' + + 'Invalid base URL: "mail://example.org". TestCafe cannot execute the test because the base URL includes the mail protocol. TestCafe supports the following protocols: http://, https:// and file://.'; + const code = RUNTIME_ERRORS.unsupportedUrlProtocol; + + expect(err.message).eql(message); + expect(err.code).eql(code); + } + }); + + it('Should raise an error if "fixture.skipJsErrors" method argument has invalid value type', () => { + const testfile = resolve('test/server/data/test-suites/skip-js-errors/fixture-invalid-argument.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The skipJsErrors options argument (string) is not of expected type (boolean, non-null object or a function).', + + callsite: ' 1 |fixture`SkipJsErrors API`\n' + + ' > 2 | .skipJsErrors(\'test\');\n' + + ' 3 |\n' + + ' 4 |test(\'test\', () => {\n' + + ' 5 |\n' + + ' 6 |})\n' + + ' 7 |\n', + }); + }); + }); + + it('Should raise an error if "fixture.skipJsErrors" method argument has invalid SkipJsErrorsOptionsObject structure', () => { + const testfile = resolve('test/server/data/test-suites/skip-js-errors/fixture-options-object-argument.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "invalidProp" option does not exist. Use the following options to configure skipJsErrors: "message", "stack", and "pageUrl".', + + callsite: ' 1 |fixture`SkipJsErrors API`\n' + + ' > 2 | .skipJsErrors({ message: \'test\', invalidProp: false });\n' + + ' 3 |\n' + + ' 4 |test(\'test\', () => {\n' + + ' 5 |\n' + + ' 6 |})\n' + + ' 7 |\n', + }); + }); + }); + + it('Should raise an error if "fixture.skipJsErrors" method argument has invalid SkipJsErrorsCallbackWithOptionsObject structure', () => { + const testfile = resolve('test/server/data/test-suites/skip-js-errors/fixture-function-with-options-object-argument.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "invalidProp" option does not exist. Use the following options to configure skipJsErrors callbacks: "fn" and "dependencies".', + + callsite: ' 1 |fixture`SkipJsErrors API`\n' + + ' > 2 | .skipJsErrors({ fn: () => true, invalidProp: false });\n' + + ' 3 |\n' + + ' 4 |test(\'test\', () => {\n' + + ' 5 |\n' + + ' 6 |})\n' + + ' 7 |\n', }); }); }); @@ -242,7 +505,7 @@ describe('API', function () { describe('test', function () { it('Should raise an error if test name is not a string', function () { - var testfile = resolve('test/server/data/test-suites/test-name-is-not-a-string/testfile.js'); + const testfile = resolve('test/server/data/test-suites/test-name-is-not-a-string/testfile.js'); return compile(testfile) .then(function () { @@ -252,8 +515,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'The test name is expected to be a string, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The test name (number) is not of expected type (string).', callsite: ' 4 |// (to treat a file as a test, it requires at least one fixture definition\n' + ' 5 |// with the string argument).\n' + @@ -261,13 +524,13 @@ describe('API', function () { ' 8 |\n' + ' > 9 |test(42, () => {\n' + ' 10 |});\n' + - ' 11 |' + ' 11 |', }); }); }); it('Should raise an error if test body is not a function', function () { - var testfile = resolve('test/server/data/test-suites/test-body-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/test-body-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -277,19 +540,19 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'The test body is expected to be a function, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The test body (string) is not of expected type (function).', callsite: ' 1 |fixture `Test body is not a function`;\n' + ' 2 |\n' + " > 3 |test('Test', 'Yo');\n" + - ' 4 |' + ' 4 |', }); }); }); it('Should raise an error if test.before is not a function', function () { - var testfile = resolve('test/server/data/test-suites/test-before-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/test-before-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -299,21 +562,21 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'test.before hook is expected to be a function, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The test.before hook (number) is not of expected type (function).', callsite: ' 1 |fixture `Fixture`;\n' + ' 2 |\n' + " > 3 |test.before(123)('Some test', () => {\n" + ' 4 |\n' + ' 5 |});\n' + - ' 6 |' + ' 6 |', }); }); }); it('Should raise an error if test.after is not a function', function () { - var testfile = resolve('test/server/data/test-suites/test-after-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/test-after-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -323,15 +586,247 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'test.after hook is expected to be a function, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The test.after hook (number) is not of expected type (function).', callsite: ' 1 |fixture `Fixture`;\n' + ' 2 |\n' + " > 3 |test.after(123)('Some test', () => {\n" + ' 4 |\n' + ' 5 |});\n' + - ' 6 |' + ' 6 |', + }); + }); + }); + + it('Should raise an error if requestHooks takes a wrong argument', function () { + const testHookArrayContainsNotRequestHookInheritor = resolve('test/server/data/test-suites/request-hooks/test-hook-array-contains-not-request-hook-inheritor.js'); + + return compile(testHookArrayContainsNotRequestHookInheritor) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testHookArrayContainsNotRequestHookInheritor, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The hook (number) is not of expected type (RequestHook subclass).', + + callsite: " 1 |import { RequestMock } from 'testcafe';\n" + + ' 2 |\n' + + ' 3 |fixture `Hook array contains not RequestHook inheritor`;\n' + + ' 4 |\n' + + " > 5 |test.requestHooks([RequestMock(), 1])('test', async t => {\n" + + ' 6 |});\n' + + ' 7 |\n', + }); + }); + }); + + it('Should clone request hooks from fixture to test', () => { + const cloneHooksFromFixtureToTest = resolve('test/server/data/test-suites/request-hooks/clone-hooks-from-fixture-to-test.js'); + + return compile(cloneHooksFromFixtureToTest) + .then(compiledData => { + const fixture = compiledData.fixtures[0]; + const test = compiledData.tests[0]; + + expect(fixture.requestHooks.length).eql(2); + expect(test.requestHooks.length).eql(3); + }); + }); + + it('Should not clone the same request hook from fixture to test twice', () => { + const shouldNotCloneSameRequestHookFromFixtureToTest = resolve('test/server/data/test-suites/request-hooks/should-not-clone-same-request-hook-from-fixture-to-test.js'); + + return compile(shouldNotCloneSameRequestHookFromFixtureToTest) + .then(compiledData => { + const fixture = compiledData.fixtures[0]; + const test = compiledData.tests[0]; + + expect(fixture.requestHooks.length).eql(2); + expect(test.requestHooks.length).eql(3); + }); + }); + + it('Should raise an error if "test.requestHooks" method calls several times', () => { + const testfile = resolve('test/server/data/test-suites/request-hooks/test-request-hooks-call-several-times.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'You cannot call the "requestHooks" method more than once. Specify an array of parameters instead.', + + callsite: ' 5 |\n' + + ' 6 |fixture `Fixture`;\n' + + ' 7 |\n' + + ' 8 |test\n' + + ' 9 | .requestHooks(logger1)\n' + + ' > 10 | .requestHooks(logger2)\n' + + ' 11 | (\'test\', async t => {});\n' + + ' 12 |', + }); + }); + }); + + it('Should collect meta data', function () { + return compile('test/server/data/test-suites/meta/testfile.js') + .then(function (compiled) { + expect(compiled.tests[0].meta.metaField1).eql('testMetaValue1'); + expect(compiled.tests[0].meta.metaField4).eql('testMetaUpdatedValue4'); + expect(compiled.tests[0].meta.metaField5).eql('testMetaValue5'); + expect(compiled.tests[1].meta.emptyField).eql(void 0); + }); + }); + + it('Should raise an error if test.meta is null', function () { + const file = resolve('test/server/data/test-suites/meta/incorrect-test-meta.js'); + + return compile(file) + .then(function () { + throw new Error('Promise rejection expected'); + }) + .catch(function (err) { + assertAPIError(err, { + stackTop: file, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'test.meta (null) is not of expected type (string or a non-null object).', + + callsite: ' 1 |fixture(\'Fixture1\')\n' + + ' 2 | .page(\'http://example.com\');\n' + + ' 3 |\n' + + ' 4 |test\n' + + ' > 5 | .meta(null)\n' + + ' 6 | (\'Fixture1Test1\', async () => {\n' + + ' 7 | // do nothing\n' + + ' 8 | });', + }); + }); + }); + + it('Should raise an error if fixture is missing', function () { + const file = resolve('test/server/data/test-suites/fixture-is-missing/testfile.js'); + + return compile(file) + .then(function () { + throw new Error('Promise rejection expected'); + }) + .catch(function (err) { + assertAPIError(err, { + stackTop: file, + + message: 'Cannot prepare tests due to the following error:\n\n' + + "The fixture of 'Test' test (null) is not of expected type (non-null object).", + + callsite: ' 1 |// fixture `Fixture`\n' + + ' 2 |\n' + + ' > 3 |test(\'Test\', () => {\n' + + ' 4 | return \'yo\';\n' + + ' 5 |});', + }); + }); + }); + + it('Should raise an error if "test.clientScripts" method takes a wrong argument', () => { + const testfile = resolve('test/server/data/test-suites/custom-client-scripts/test-client-scripts-has-wrong-type.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The client script (number) is not of expected type (string or a client script initializer).', + + callsite: ' 1 |fixture `Fixture`;\n' + + ' 2 |\n' + + ' 3 |test\n' + + ' > 4 | .clientScripts(8)\n' + + ' 5 | (\'test\', async t => {});', + }); + }); + }); + + it('Should raise an error if "test.clientScripts" method calls several times', () => { + const testfile = resolve('test/server/data/test-suites/custom-client-scripts/test-client-scripts-call-several-times.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'You cannot call the "clientScripts" method more than once. Specify an array of parameters instead.', + + callsite: ' 1 |fixture `Fixture`;\n' + + ' 2 |\n' + + ' 3 |test\n' + + ' 4 | .clientScripts(\'script1.js\')\n' + + ' > 5 | .clientScripts(\'script2.js\')\n' + + ' 6 | (\'test\', async t => {});\n' + + ' 7 |', + }); + }); + }); + + it('Should raise an error if "test.timeouts" method takes a wrong argument', () => { + const testfile = resolve('test/server/data/test-suites/test-timeouts/testfile.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'test.timeouts (number) is not of expected type (test timeouts initializer).', + + callsite: ' 1 |fixture `Test timeouts`;\n' + + ' 2 |\n' + + ' 3 |test\n' + + ' > 4 | .timeouts(20000)\n' + + ' 5 | (\'test\', async () => {});\n' + + ' 6 |', + }); + }); + }); + + it('Should raise an error if "test.timeouts.pageLoadTimeout" is not a non-negative number', () => { + const testfile = resolve('test/server/data/test-suites/test-timeouts/page-load-timeout/testfile.js'); + + return compile(testfile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'test.timeouts.pageLoadTimeout (-1) is not of expected type (non-negative number).', + + callsite: ' 1 |fixture `Page Load Timeout`;\n' + + ' 2 |\n' + + ' 3 |test\n' + + ' > 4 | .timeouts({ pageLoadTimeout: -1 })\n' + + ' 5 | (\'test\', async () => {});\n' + + ' 6 |', }); }); }); @@ -339,7 +834,7 @@ describe('API', function () { describe('Selector', function () { it('Should raise an error if Selector initialized with wrong type', function () { - var testfile = resolve('test/server/data/test-suites/selector-arg-is-not-a-function-or-string/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-arg-is-not-a-function-or-string/testfile.js'); return compile(testfile) .then(function () { @@ -349,9 +844,9 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'Selector is expected to be initialized with a function, CSS selector string, another Selector, ' + - 'node snapshot or a Promise returned by a Selector, but number was passed.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Cannot initialize a Selector because Selector is number, and not one of the following: a CSS selector string, ' + + 'a Selector object, a node snapshot, a function, or a Promise returned by a Selector.', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -361,13 +856,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise an error if Selector `visibilityCheck` option is not a boolean value', function () { - var testfile = resolve('test/server/data/test-suites/selector-visibility-check-opt-not-bool/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-visibility-check-opt-not-bool/testfile.js'); return compile(testfile) .then(function () { @@ -377,8 +872,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"visibilityCheck" option is expected to be a boolean, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "visibilityCheck" option (number) is not of expected type (boolean).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -387,13 +882,13 @@ describe('API', function () { ' > 5 |Selector(() => {}).with({ visibilityCheck: 42 });\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector `timeout` option is not a non-negative number', function () { - var testfile = resolve('test/server/data/test-suites/selector-timeout-is-not-non-negative-value/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-timeout-is-not-non-negative-value/testfile.js'); return compile(testfile) .then(function () { @@ -403,8 +898,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"timeout" option is expected to be a non-negative number, but it was -5.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "timeout" option (-5) is not of expected type (non-negative number).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -413,13 +908,65 @@ describe('API', function () { ' > 5 |Selector(() => {}).with({ timeout: -5 });\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', + }); + }); + }); + + it('Should raise `it was NaN` error if Selector.nth() `index` argument is NaN', function () { + const testfile = resolve('test/server/data/test-suites/selector-nth-arg-is-nan-value/testfile.js'); + + return compile(testfile) + .then(function () { + throw new Error('Promise rejection expected'); + }) + .catch(function (err) { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "index" argument (NaN) is not of expected type (number).', + + callsite: " 1 |import { Selector } from 'testcafe';\n" + + ' 2 |\n' + + ' 3 |fixture `Test`;\n' + + ' 4 |\n' + + ' > 5 |Selector(() => {}).nth(NaN);\n' + + ' 6 |\n' + + " 7 |test('yo', () => {\n" + + ' 8 |});', + }); + }); + }); + + it('Should raise `it was Infinity` error if Selector.nth() `index` argument is Infinity', function () { + const testfile = resolve('test/server/data/test-suites/selector-nth-arg-is-infinity-value/testfile.js'); + + return compile(testfile) + .then(function () { + throw new Error('Promise rejection expected'); + }) + .catch(function (err) { + assertAPIError(err, { + stackTop: testfile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "index" argument (Infinity) is not of expected type (number).', + + callsite: " 1 |import { Selector } from 'testcafe';\n" + + ' 2 |\n' + + ' 3 |fixture `Test`;\n' + + ' 4 |\n' + + ' > 5 |Selector(() => {}).nth(Infinity);\n' + + ' 6 |\n' + + " 7 |test('yo', () => {\n" + + ' 8 |});', }); }); }); it('Should raise an error if Selector.nth() `index` argument is not a number', function () { - var testfile = resolve('test/server/data/test-suites/selector-nth-arg-is-a-number-value/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-nth-arg-is-a-number-value/testfile.js'); return compile(testfile) .then(function () { @@ -429,8 +976,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"index" argument is expected to be a number, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "index" argument (string) is not of expected type (number).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -439,13 +986,13 @@ describe('API', function () { ' > 5 |Selector(() => {}).nth(\'hey\');\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.withText `text` argument is not a RegExp or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-with-text-arg-is-not-regexp-or-string/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-with-text-arg-is-not-regexp-or-string/testfile.js'); return compile(testfile) .then(function () { @@ -455,8 +1002,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"text" argument is expected to be a string or a regular expression, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "text" argument (object) is not of expected type (string or a regular expression).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -465,13 +1012,13 @@ describe('API', function () { ' > 5 |Selector(() => {}).withText({});\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.withAttribute `attrName` argument is not a RegExp or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-with-attr-arg-is-not-regexp-or-string/attrName.js'); + const testfile = resolve('test/server/data/test-suites/selector-with-attr-arg-is-not-regexp-or-string/attrName.js'); return compile(testfile) .then(function () { @@ -481,8 +1028,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"attrName" argument is expected to be a string or a regular expression, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "attrName" argument (object) is not of expected type (string or a regular expression).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -491,13 +1038,13 @@ describe('API', function () { ' > 5 |Selector(() => {}).withAttribute(null);\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.withAttribute `attrValue` argument is not a RegExp or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-with-attr-arg-is-not-regexp-or-string/attrValue.js'); + const testfile = resolve('test/server/data/test-suites/selector-with-attr-arg-is-not-regexp-or-string/attrValue.js'); return compile(testfile) .then(function () { @@ -507,8 +1054,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"attrValue" argument is expected to be a string or a regular expression, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "attrValue" argument (number) is not of expected type (string or a regular expression).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -517,13 +1064,13 @@ describe('API', function () { ' > 5 |Selector(() => {}).withAttribute(/class/, -100);\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.filter `filter` argument is not a function or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-filter-arg-is-not-a-function-or-string/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-filter-arg-is-not-a-function-or-string/testfile.js'); return compile(testfile) .then(function () { @@ -533,8 +1080,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"filter" argument is expected to be a string or a function, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "filter" argument (object) is not of expected type (string or a function).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -543,13 +1090,13 @@ describe('API', function () { " > 5 |Selector('span').filter({});\n" + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.find `filter` argument is not a function or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-find-arg-is-not-a-string-or-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-find-arg-is-not-a-string-or-function/testfile.js'); return compile(testfile) .then(function () { @@ -559,8 +1106,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"filter" argument is expected to be a string or a function, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "filter" argument (object) is not of expected type (string or a function).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -569,13 +1116,13 @@ describe('API', function () { ' > 5 |Selector(\'span\').find({});\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.parent `filter` argument is not a function or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-parent-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-parent-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -585,8 +1132,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"filter" argument is expected to be a string, function or a number, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "filter" argument (object) is not of expected type (string, function or a number).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -595,13 +1142,13 @@ describe('API', function () { ' > 5 |Selector(\'span\').parent({});\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.child `filter` argument is not a function or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-child-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-child-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -611,8 +1158,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"filter" argument is expected to be a string, function or a number, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "filter" argument (object) is not of expected type (string, function or a number).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -621,13 +1168,13 @@ describe('API', function () { ' > 5 |Selector(\'span\').child({});\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.sibling `filter` argument is not a function or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-sibling-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-sibling-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -637,8 +1184,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"filter" argument is expected to be a string, function or a number, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "filter" argument (object) is not of expected type (string, function or a number).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -647,13 +1194,13 @@ describe('API', function () { ' > 5 |Selector(\'span\').sibling({});\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.nextSibling `filter` argument is not a function or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-next-sibling-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-next-sibling-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -663,8 +1210,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"filter" argument is expected to be a string, function or a number, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "filter" argument (object) is not of expected type (string, function or a number).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -673,14 +1220,14 @@ describe('API', function () { ' > 5 |Selector(\'span\').nextSibling({});\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.prevSibling `filter` argument is not a function or string', function () { - var testfile = resolve('test/server/data/test-suites/selector-prev-sibling-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-prev-sibling-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -690,8 +1237,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"filter" argument is expected to be a string, function or a number, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "filter" argument (object) is not of expected type (string, function or a number).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -700,13 +1247,13 @@ describe('API', function () { ' > 5 |Selector(\'span\').prevSibling({});\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); it('Should raise an error if Selector.addCustomDOMProperties argument is not object', function () { - var testfile = resolve('test/server/data/test-suites/selector-add-custom-dom-properties-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-add-custom-dom-properties-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -716,8 +1263,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"addCustomDOMProperties" option is expected to be a non-null object, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "addCustomDOMProperties" option (number) is not of expected type (non-null object).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -727,13 +1274,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise error if at least one of Selector custom DOM properties is not function', function () { - var testfile = resolve('test/server/data/test-suites/selector-custom-dom-property-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-custom-dom-property-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -743,8 +1290,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - "Custom DOM properties method 'prop1' is expected to be a function, but it was number.", + message: 'Cannot prepare tests due to the following error:\n\n' + + "The custom DOM properties method 'prop1' (number) is not of expected type (function).", callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -753,13 +1300,13 @@ describe('API', function () { ' 5 |\n' + " 6 |test('yo', () => {\n" + ' 7 |});\n' + - ' 8 |' + ' 8 |', }); }); }); it('Should raise error if Selector.addCustomMethods argument is not object', function () { - var testfile = resolve('test/server/data/test-suites/selector-add-custom-methods-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-add-custom-methods-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -769,8 +1316,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"addCustomMethods" option is expected to be a non-null object, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "addCustomMethods" option (number) is not of expected type (non-null object).', callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -780,13 +1327,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise error if at least one of custom methods is not function', function () { - var testfile = resolve('test/server/data/test-suites/selector-custom-dom-method-incorrect-arg-type/testfile.js'); + const testfile = resolve('test/server/data/test-suites/selector-custom-dom-method-incorrect-arg-type/testfile.js'); return compile(testfile) .then(function () { @@ -796,8 +1343,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - "Custom method 'prop1' is expected to be a function, but it was number.", + message: 'Cannot prepare tests due to the following error:\n\n' + + "The custom method 'prop1' (number) is not of expected type (function).", callsite: " 1 |import { Selector } from 'testcafe';\n" + ' 2 |\n' + @@ -806,7 +1353,7 @@ describe('API', function () { ' 5 |\n' + " 6 |test('yo', () => {\n" + ' 7 |});\n' + - ' 8 |' + ' 8 |', }); }); }); @@ -814,7 +1361,7 @@ describe('API', function () { describe('ClientFunction', function () { it('Should raise an error if ClientFunction argument is not a function', function () { - var testfile = resolve('test/server/data/test-suites/client-fn-arg-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/client-fn-arg-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -824,8 +1371,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'ClientFunction code is expected to be specified as a function, but number was passed.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Cannot initialize a ClientFunction because ClientFunction is number, and not a function.', callsite: " 1 |import { ClientFunction } from 'testcafe';\n" + ' 2 |\n' + @@ -835,13 +1382,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise an error if ClientFunction argument is not a function (if called as ctor)', function () { - var testfile = resolve('test/server/data/test-suites/client-fn-arg-is-not-a-function-as-ctor/testfile.js'); + const testfile = resolve('test/server/data/test-suites/client-fn-arg-is-not-a-function-as-ctor/testfile.js'); return compile(testfile) .then(function () { @@ -851,8 +1398,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'ClientFunction code is expected to be specified as a function, but number was passed.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Cannot initialize a ClientFunction because ClientFunction is number, and not a function.', callsite: " 1 |import { ClientFunction } from 'testcafe';\n" + ' 2 |\n' + @@ -862,13 +1409,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise an error if ClientFunction uses async function', function () { - var testfile = resolve('test/server/data/test-suites/async-function-in-client-fn/testfile.js'); + const testfile = resolve('test/server/data/test-suites/async-function-in-client-fn/testfile.js'); return compile(testfile) .then(function () { @@ -878,7 +1425,7 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + + message: 'Cannot prepare tests due to the following error:\n\n' + 'ClientFunction code, arguments or dependencies cannot contain generators or "async/await" syntax (use Promises instead).', callsite: " 1 |import { ClientFunction } from 'testcafe';\n" + @@ -889,13 +1436,13 @@ describe('API', function () { ' 6 |});\n' + ' 7 |\n' + " 8 |test('yo', () => {\n" + - ' 9 |});\n' + ' 9 |});\n', }); }); }); it('Should raise an error if ClientFunction uses generator', function () { - var testfile = resolve('test/server/data/test-suites/generator-in-client-fn/testfile.js'); + const testfile = resolve('test/server/data/test-suites/generator-in-client-fn/testfile.js'); return compile(testfile) .then(function () { @@ -905,7 +1452,7 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + + message: 'Cannot prepare tests due to the following error:\n\n' + 'ClientFunction code, arguments or dependencies cannot contain generators or "async/await" syntax (use Promises instead).', callsite: " 1 |import { ClientFunction } from 'testcafe';\n" + @@ -917,13 +1464,13 @@ describe('API', function () { ' 7 |});\n' + ' 8 |\n' + " 9 |test('yo', () => {\n" + - ' 10 |});' + ' 10 |});', }); }); }); it('Should raise an error if ClientFunction options is not an object', function () { - var testfile = resolve('test/server/data/test-suites/client-fn-options-not-object/testfile.js'); + const testfile = resolve('test/server/data/test-suites/client-fn-options-not-object/testfile.js'); return compile(testfile) .then(function () { @@ -933,8 +1480,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"options" argument is expected to be a non-null object, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "options" argument (number) is not of expected type (non-null object).', callsite: " 1 |import { ClientFunction } from 'testcafe';\n" + ' 2 |\n' + @@ -943,13 +1490,13 @@ describe('API', function () { ' > 5 |ClientFunction(() => {}).with(123);\n' + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});\n' + ' 8 |});\n', }); }); }); it('Should raise an error if ClientFunction "dependencies" is not an object', function () { - var testfile = resolve('test/server/data/test-suites/client-fn-dependencies-not-object/testfile.js'); + const testfile = resolve('test/server/data/test-suites/client-fn-dependencies-not-object/testfile.js'); return compile(testfile) .then(function () { @@ -959,21 +1506,20 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"dependencies" option is expected to be a non-null object, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "dependencies" option (string) is not of expected type (non-null object).', callsite: " 1 |import { ClientFunction } from 'testcafe';\n" + ' 2 |\n' + ' 3 |fixture `Test`;\n' + ' 4 |\n' + - " > 5 |var selectYo = ClientFunction(() => document.querySelector('#yo'), { dependencies: '42' });\n" + " > 5 |var selectYo = ClientFunction(() => document.querySelector('#yo'), { dependencies: '42' });\n", }); }); }); - it('Should raise an error if ClientFunction `boundTestRun` option is not TestController', function () { - var testfile = resolve('test/server/data/test-suites/client-fn-bound-test-run-not-t/testfile.js'); + const testfile = resolve('test/server/data/test-suites/client-fn-bound-test-run-not-t/testfile.js'); return compile(testfile) .then(function () { @@ -983,8 +1529,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'The "boundTestRun" option value is expected to be a test controller.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Cannot resolve the "boundTestRun" option because its value is not a test controller.', callsite: " 1 |import { ClientFunction } from 'testcafe';\n" + ' 2 |\n' + @@ -993,7 +1539,7 @@ describe('API', function () { " > 5 |ClientFunction(() => {}).with({ boundTestRun: 'yo' });\n" + ' 6 |\n' + " 7 |test('yo', () => {\n" + - ' 8 |});' + ' 8 |});', }); }); }); @@ -1001,8 +1547,8 @@ describe('API', function () { }); describe('Role', function () { - it('Should raise an error if Role "loginPage" is not a string', function () { - var testfile = resolve('test/server/data/test-suites/role-login-page-is-not-a-string/testfile.js'); + it('Should raise an error if Role "loginUrl" is not a string', function () { + const testfile = resolve('test/server/data/test-suites/role-login-page-is-not-a-string/testfile.js'); return compile(testfile) .then(function () { @@ -1012,8 +1558,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"loginPage" argument is expected to be a string, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "loginUrl" argument (number) is not of expected type (string).', callsite: " 1 |import { Role } from 'testcafe';\n" + ' 2 |\n' + @@ -1023,13 +1569,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise an error if Role "initFn" is not a string', function () { - var testfile = resolve('test/server/data/test-suites/role-init-fn-is-not-a-function/testfile.js'); + const testfile = resolve('test/server/data/test-suites/role-init-fn-is-not-a-function/testfile.js'); return compile(testfile) .then(function () { @@ -1039,8 +1585,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"initFn" argument is expected to be a function, but it was number.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "initFn" argument (number) is not of expected type (function).', callsite: " 1 |import { Role } from 'testcafe';\n" + ' 2 |\n' + @@ -1050,13 +1596,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise an error if Role "options" is not an object', function () { - var testfile = resolve('test/server/data/test-suites/role-options-is-not-an-object/testfile.js'); + const testfile = resolve('test/server/data/test-suites/role-options-is-not-an-object/testfile.js'); return compile(testfile) .then(function () { @@ -1066,8 +1612,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"options" argument is expected to be a non-null object, but it was string.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "options" argument (string) is not of expected type (non-null object).', callsite: " 1 |import { Role } from 'testcafe';\n" + ' 2 |\n' + @@ -1077,13 +1623,13 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); it('Should raise an error if Role "option.preserveUrl" is not a boolean', function () { - var testfile = resolve('test/server/data/test-suites/role-preserve-url-option-is-not-a-boolean/testfile.js'); + const testfile = resolve('test/server/data/test-suites/role-preserve-url-option-is-not-a-boolean/testfile.js'); return compile(testfile) .then(function () { @@ -1093,8 +1639,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - '"preserveUrl" option is expected to be a boolean, but it was object.', + message: 'Cannot prepare tests due to the following error:\n\n' + + 'The "preserveUrl" option (object) is not of expected type (boolean).', callsite: " 1 |import { Role } from 'testcafe';\n" + ' 2 |\n' + @@ -1104,15 +1650,15 @@ describe('API', function () { ' 6 |\n' + " 7 |test('yo', () => {\n" + ' 8 |});\n' + - ' 9 |' + ' 9 |', }); }); }); }); describe('TestController import', function () { - it('Should raise an error if TestControllerProxy can not resolve test run', function () { - var testfile = resolve('test/server/data/test-suites/cant-resolve-test-run-proxy-context/testfile.js'); + it('Should raise an error if TestControllerProxy cannot resolve test run', function () { + const testfile = resolve('test/server/data/test-suites/cannot-resolve-test-run-proxy-context/testfile.js'); return compile(testfile) .then(function () { @@ -1122,8 +1668,8 @@ describe('API', function () { assertAPIError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - "Cannot implicitly resolve the test run in the context of which the test controller action should be executed. Use test function's 't' argument instead.", + message: 'Cannot prepare tests due to the following error:\n\n' + + "The action does not have implicit test controller access. Reference the 't' object to gain it.", callsite: ' 1 |import { t } from \'testcafe\';\n' + ' 2 |\n' + @@ -1133,9 +1679,210 @@ describe('API', function () { ' 6 |\n' + ' 7 |test(\'Some test\', async () => {\n' + ' 8 |\n' + - ' 9 |});' + ' 9 |});', }); }); }); }); + + describe('Request Hooks', () => { + describe('Should raise errors for wrong RequestLogger construction', () => { + it('Cannot stringify the request body', () => { + const testFile = resolve('test/server/data/test-suites/request-hooks/request-logger/cannot-stringify-request-body.js'); + + return compile(testFile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testFile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Attempt to configure a request hook resulted in the following error:\n\n' + + 'RequestLogger: Cannot stringify the request body because it is not logged. Specify { logRequestBody: true } in log options.', + + callsite: ' 1 |import { RequestLogger } from \'testcafe\';\n' + + ' 2 |\n' + + ' 3 |fixture `Fixture`;\n' + + ' 4 |\n' + + " > 5 |const logger = new RequestLogger('', {\n" + + ' 6 | logRequestBody: false,\n' + + ' 7 | stringifyRequestBody: true\n' + + ' 8 |});', + }); + }); + }); + + it('Cannot stringify the response body', () => { + const testFile = resolve('test/server/data/test-suites/request-hooks/request-logger/cannot-stringify-response-body.js'); + + return compile(testFile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testFile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Attempt to configure a request hook resulted in the following error:\n\n' + + 'RequestLogger: Cannot stringify the response body because it is not logged. Specify { logResponseBody: true } in log options.', + + callsite: ' 1 |import { RequestLogger } from \'testcafe\';\n' + + ' 2 |\n' + + ' 3 |fixture `Fixture`;\n' + + ' 4 |\n' + + " > 5 |const logger = new RequestLogger('', {\n" + + ' 6 | logResponseBody: false,\n' + + ' 7 | stringifyResponseBody: true\n' + + ' 8 |});', + }); + }); + }); + }); + + describe('Should raise errors for wrong RequestMock api order call', () => { + it("The 'respond' method was not called after 'onRequestTo'", () => { + const testFile = resolve('test/server/data/test-suites/request-hooks/request-mock/respond-was-not-called-after-on-request-to.js'); + + return compile(testFile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testFile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Attempt to configure a request hook resulted in the following error:\n\n' + + "RequestMock: The 'respond' method was not called after 'onRequestTo'. You must call the 'respond' method to provide the mocked response.", + + callsite: ' 1 |import { RequestMock } from \'testcafe\';\n' + + ' 2 |\n' + + ' 3 |fixture `Fixture`;\n' + + ' 4 |\n' + + ' > 5 |const mock = RequestMock().onRequestTo({}).onRequestTo({});\n' + + ' 6 |\n' + + ' 7 |test(\'test\', async t => {});\n' + + ' 8 |\n', + }); + }); + }); + + it("The 'onRequestTo' method was not called before 'respond'", () => { + const testFile = resolve('test/server/data/test-suites/request-hooks/request-mock/on-request-to-was-not-called-before-respond.js'); + + return compile(testFile) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertAPIError(err, { + stackTop: testFile, + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Attempt to configure a request hook resulted in the following error:\n\n' + + "RequestMock: The 'onRequestTo' method was not called before 'respond'. You must call the 'onRequestTo' method to provide the URL requests to which are mocked.", + + callsite: ' 1 |import { RequestMock } from \'testcafe\';\n' + + ' 2 |\n' + + ' 3 |fixture `Fixture`;\n' + + ' 4 |\n' + + ' > 5 |const mock = RequestMock().respond(() => {}).onRequestTo({});\n' + + ' 6 |\n' + + ' 7 |test(\'test\', async t => {});\n' + + ' 8 |', + }); + }); + }); + }); + }); + + describe('createTestCafe', () => { + function getMockedCreateTestCafe () { + const TestCafe = sinon.stub().returns({}); + + const createTestCafe = proxyquire('../..', { + './testcafe': TestCafe, + 'async-exit-hook': () => {}, + + './configuration/utils': { + getValidHostname: val => val, + getValidPort: val => val, + }, + }); + + return { + TestCafe, + createTestCafe, + }; + } + + it('Should accept configuration as an arguments array', async () => { + const { createTestCafe, TestCafe } = getMockedCreateTestCafe(); + + await createTestCafe('my-host', 1337, 1338, { test: 42 }, true, true); + + const configuration = TestCafe.firstCall.args[0]; + + expect(configuration.getOption(OPTION_NAMES.hostname)).equal('my-host'); + expect(configuration.getOption(OPTION_NAMES.port1)).equal(1337); + expect(configuration.getOption(OPTION_NAMES.port2)).equal(1338); + expect(configuration.getOption(OPTION_NAMES.ssl)).deep.equal({ test: 42 }); + expect(configuration.getOption(OPTION_NAMES.developmentMode)).be.true; + expect(configuration.getOption(OPTION_NAMES.retryTestPages)).be.true; + + }); + + it('Should accept configuration as an object', async () => { + const { createTestCafe, TestCafe } = getMockedCreateTestCafe(); + + await createTestCafe({ + hostname: 'my-host', + port1: 1337, + port2: 1338, + + ssl: { + test: 42, + }, + + developmentMode: true, + retryTestPages: true, + disableHttp2: true, + }); + + const configuration = TestCafe.firstCall.args[0]; + + expect(configuration.getOption(OPTION_NAMES.hostname)).equal('my-host'); + expect(configuration.getOption(OPTION_NAMES.port1)).equal(1337); + expect(configuration.getOption(OPTION_NAMES.port2)).equal(1338); + expect(configuration.getOption(OPTION_NAMES.ssl)).deep.equal({ test: 42 }); + expect(configuration.getOption(OPTION_NAMES.developmentMode)).be.true; + expect(configuration.getOption(OPTION_NAMES.retryTestPages)).be.true; + expect(configuration.getOption(OPTION_NAMES.disableHttp2)).be.true; + }); + }); + + describe('API Methods Validation', () => { + it('Should checks all methods', async () => { + const GETTER_API_METHODS = ['only', 'skip', 'disablePageReloads', 'enablePageReloads', 'disablePageCaching', 'disableConcurrency']; + const FUNCTIONS_API_METHODS = ['page', 'skipJsErrors', 'httpAuth', 'meta', 'before', 'after', 'beforeEach', 'afterEach', 'requestHooks', 'clientScripts']; + + for (const apiMethod of Fixture.API_LIST) { + if (!GETTER_API_METHODS.includes(apiMethod.apiProp) && !FUNCTIONS_API_METHODS.includes(apiMethod.apiProp)) { + throw new Error(`Please, check the "${apiMethod.srcProp}" method. + If the method doesn't accept any arguments, ensure that the method is implemented as a 'getter' and add the method name to GETTER_API_METHODS. + If the method accepts arguments, ensure that the method is implemented as a 'function' and add the method name to FUNCTIONS_API_METHODS. + `); + } + + if (GETTER_API_METHODS.includes(apiMethod.apiProp) && apiMethod.accessor !== 'getter') + throw new Error(`Make sure that the method "${apiMethod.srcProp}" is implemented as a "getter"`); + + if (FUNCTIONS_API_METHODS.includes(apiMethod.apiProp) && apiMethod.accessor) + throw new Error(`Make sure that the method "${apiMethod.srcProp}" is implemented as a "function"`); + } + }); + }); }); diff --git a/test/server/bootstrapper-test.js b/test/server/bootstrapper-test.js new file mode 100644 index 00000000000..81e2ea5b38b --- /dev/null +++ b/test/server/bootstrapper-test.js @@ -0,0 +1,193 @@ +const { expect } = require('chai'); +const BrowserConnection = require('../../lib/browser/connection'); +const Bootstrapper = require('../../lib/runner/bootstrapper'); +const Test = require('../../lib/api/structure/test'); +const delay = require('../../lib/utils/delay'); + +const { + browserConnectionGatewayMock, + configurationMock, + createBrowserProviderMock, +} = require('./helpers/mocks'); + +describe('Bootstrapper', () => { + describe('.createRunnableConfiguration()', () => { + let bootstrapper = null; + + beforeEach(() => { + bootstrapper = new Bootstrapper({ + browserConnectionGateway: browserConnectionGatewayMock, + configuration: configurationMock, + }); + + bootstrapper.browserInitTimeout = 100; + bootstrapper.TESTS_COMPILATION_UPPERBOUND = 0; + + bootstrapper.browsers = [ new BrowserConnection(browserConnectionGatewayMock, { provider: createBrowserProviderMock({ local: false }) }) ]; + + bootstrapper._compileTests = async () => { + await delay(1500); + + return [ new Test({ currentFixture: void 0 }) ]; + }; + }); + + it('Browser connection error message should include hint that tests compilation takes too long', async function () { + this.timeout(3000); + + try { + await bootstrapper.createRunnableConfiguration(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).contains('Tests took too long to compile'); + } + }); + + it('Should raise an error if fixture.globalBefore is not a function', async function () { + bootstrapper.hooks = { + fixture: { + before: 'yo', + }, + }; + + try { + await bootstrapper.createRunnableConfiguration(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql('Cannot prepare tests due to the following error:\n\n' + + 'The fixture.globalBefore hook (string) is not of expected type (function).'); + } + }); + + it('Should raise an error if fixture.globalAfter is not a function', async function () { + bootstrapper.hooks = { + fixture: { + after: 'yo', + }, + }; + + try { + await bootstrapper.createRunnableConfiguration(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql('Cannot prepare tests due to the following error:\n\n' + + 'The fixture.globalAfter hook (string) is not of expected type (function).'); + } + }); + + it('Should raise an error if test.globalBefore is not a function', async function () { + bootstrapper.hooks = { + test: { + before: 'yo', + }, + }; + + try { + await bootstrapper.createRunnableConfiguration(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql('Cannot prepare tests due to the following error:\n\n' + + 'The test.globalBefore hook (string) is not of expected type (function).'); + } + }); + + it('Should raise an error if test.globalAfter is not a function', async function () { + bootstrapper.hooks = { + test: { + after: 'yo', + }, + }; + + try { + await bootstrapper.createRunnableConfiguration(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql('Cannot prepare tests due to the following error:\n\n' + + 'The test.globalAfter hook (string) is not of expected type (function).'); + } + }); + + it("Should disable native automation mode if it's necessary", async () => { + let remoteBrowserConnections = []; + let automatedBrowserConnections = []; + + const chromeBrowserInfoMock = { + provider: { + supportNativeAutomation: () => true, + }, + }; + + const firefoxBrowserInfoMock = { + provider: { + supportNativeAutomation: () => false, + }, + }; + + const remoteBrowserConnectionMock = { + isNativeAutomationEnabled: () => false, + }; + + bootstrapper.configuration.clear(); + bootstrapper._disableNativeAutomationIfNecessary(remoteBrowserConnections, automatedBrowserConnections); + expect(bootstrapper.configuration._mergedOptions).to.be.undefined; + + remoteBrowserConnections = [remoteBrowserConnectionMock]; + + bootstrapper._disableNativeAutomationIfNecessary(remoteBrowserConnections, automatedBrowserConnections); + expect(bootstrapper.configuration._mergedOptions).eql({ disableNativeAutomation: true }); + bootstrapper.configuration.clear(); + + remoteBrowserConnections = []; + automatedBrowserConnections = [chromeBrowserInfoMock]; + + bootstrapper._disableNativeAutomationIfNecessary(remoteBrowserConnections, automatedBrowserConnections); + expect(bootstrapper.configuration._mergedOptions).to.be.undefined; + bootstrapper.configuration.clear(); + + automatedBrowserConnections = [chromeBrowserInfoMock, firefoxBrowserInfoMock]; + + bootstrapper._disableNativeAutomationIfNecessary(remoteBrowserConnections, automatedBrowserConnections); + expect(bootstrapper.configuration._mergedOptions).eql({ disableNativeAutomation: true }); + bootstrapper.configuration.clear(); + }); + + it('Should throw an error if browser is opened with the "userProfile" option in the Native Automation mode', async function () { + try { + bootstrapper.browsers = [{ + alias: 'chrome', + browserOption: { userProfile: true }, + provider: { + isLocalBrowser: () => true, + supportNativeAutomation: () => true, + }, + }, { + alias: 'edge', + browserOption: { userProfile: true }, + provider: { + isLocalBrowser: () => true, + supportNativeAutomation: () => true, + }, + }]; + + await bootstrapper.createRunnableConfiguration(); + + throw new Error('Promise rejection expected'); + } + catch (err) { + expect(err.message).eql('Cannot initialize the test run. When TestCafe uses native automation, it can only ' + + 'launch browsers with an empty user profile. Disable native automation, or remove ' + + 'the "userProfile" suffix from the following browser aliases: "chrome, edge".'); + } + }); + }); +}); diff --git a/test/server/browser-connection-gateway-test.js b/test/server/browser-connection-gateway-test.js new file mode 100644 index 00000000000..6fc4ea40ef7 --- /dev/null +++ b/test/server/browser-connection-gateway-test.js @@ -0,0 +1,27 @@ +const BrowserConnectionGateway = require('../../lib/browser/connection/gateway'); +const { expect } = require('chai'); +const { proxyMock } = require('./helpers/mocks'); + +describe('BrowserConnectionGateway', function () { + it('Should not raise an error on multiple initialization (GH-7711)', function () { + const gateway = new BrowserConnectionGateway(proxyMock); + let errorIsRaised = false; + let initializationCount = 0; + + gateway.on('initialized', () => { + initializationCount++; + }); + + try { + gateway.initialize(); + gateway.initialize(); + gateway.initialize(); + } + catch (e) { + errorIsRaised = true; + } + + expect(errorIsRaised).to.be.false; + expect(initializationCount).eql(1); + }); +}); diff --git a/test/server/browser-connection-test.js b/test/server/browser-connection-test.js index d511ab8a0b0..17c974dcdeb 100644 --- a/test/server/browser-connection-test.js +++ b/test/server/browser-connection-test.js @@ -1,30 +1,20 @@ -var expect = require('chai').expect; -var Promise = require('pinkie'); -var promisify = require('../../lib/utils/promisify'); -var request = require('request'); -var createTestCafe = require('../../lib/'); -var COMMAND = require('../../lib/browser/connection/command'); -var browserProviderPool = require('../../lib/browser/provider/pool'); +const { expect } = require('chai'); +const fetch = require('node-fetch'); +const { noop } = require('lodash'); +const createTestCafe = require('../../lib/'); +const COMMAND = require('../../lib/browser/connection/command'); +const browserProviderPool = require('../../lib/browser/provider/pool'); +const BrowserConnectionStatus = require('../../lib/browser/connection/status'); - -var promisedRequest = promisify(request); +const { createBrowserProviderMock } = require('./helpers/mocks'); describe('Browser connection', function () { - var testCafe = null; - var connection = null; - var origRemoteBrowserProvider = null; - - var remoteBrowserProviderMock = { - openBrowser: function () { - return Promise.resolve(); - }, + let testCafe = null; + let connection = null; + let originRemoteBrowserProvider = null; - closeBrowser: function () { - return Promise.resolve(); - } - }; + const remoteBrowserProviderMock = createBrowserProviderMock(); - // Fixture setup/teardown before(function () { this.timeout(20000); @@ -35,20 +25,18 @@ describe('Browser connection', function () { return browserProviderPool.getProvider('remote'); }) .then(function (remoteBrowserProvider) { - origRemoteBrowserProvider = remoteBrowserProvider; + originRemoteBrowserProvider = remoteBrowserProvider; browserProviderPool.addProvider('remote', remoteBrowserProviderMock); }); }); after(function () { - browserProviderPool.addProvider('remote', origRemoteBrowserProvider); + browserProviderPool.addProvider('remote', originRemoteBrowserProvider); return testCafe.close(); }); - - // Test setup/teardown beforeEach(function () { return testCafe .createBrowserConnection() @@ -57,87 +45,92 @@ describe('Browser connection', function () { }); }); - afterEach(function () { + afterEach(async function () { connection._forceIdle(); - connection.close(); - }); + await connection.close(); + }); - // Tests it('Should fire "ready" event and redirect to idle page once established', function () { - var eventFired = false; + let eventFired = false; connection.on('ready', function () { eventFired = true; }); - var options = { - url: connection.url, - followRedirect: false, - headers: { + const options = { + redirect: 'manual', + headers: { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 ' + - '(KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36' - } + '(KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36', + }, }; - return promisedRequest(options) + return fetch(connection.url, options) .then(function (res) { expect(eventFired).to.be.true; - expect(connection.ready).to.be.true; - expect(connection.userAgent).eql('Chrome 41.0.2227 / Mac OS X 10.10.1'); - expect(res.statusCode).eql(302); - expect(res.headers['location']).eql(connection.idleUrl); + expect(connection.status).eql(BrowserConnectionStatus.opened); + expect(connection.userAgent).eql('Chrome 41.0.2227.1 / macOS 10.10.1'); + expect(res.status).eql(302); + expect(res.headers.get('location')).eql(connection.idleUrl); }); }); it('Should respond with error if connection was established twice', function () { - return promisedRequest(connection.url) - .then(function () { - return promisedRequest(connection.url); - }) - .then(function (res) { - expect(res.statusCode).eql(500); - expect(res.body).eql('The connection is already established.'); - }); + return fetch(connection.url) + .then(() => fetch(connection.url) + .then(res => { + return res.text() + .then((body) => { + expect(res.status).to.eql(500); + expect(body).to.eql('The connection is already established.'); + }); + }) + ); }); it('Should fire "error" event on browser disconnection', function (done) { connection.HEARTBEAT_TIMEOUT = 0; connection.on('error', function (error) { - expect(error.message).eql('The Chrome 41.0.2227 / Mac OS X 10.10.1 browser disconnected. This problem may ' + - 'appear when a browser hangs or is closed, or due to network issues.'); + expect(error.message).eql('The Chrome 41.0.2227.1 / macOS 10.10.1 browser disconnected. If you did not ' + + 'close the browser yourself, browser performance or network issues may be at fault.'); done(); }); - var options = { - url: connection.url, + const options = { followRedirect: false, headers: { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 ' + - '(KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36' - } + '(KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36', + }, }; - request(options); + fetch(connection.url, options); }); it('Should provide status', function () { function createBrowserJobMock (urls) { return { - popNextTestRunUrl: function () { - var url = urls.shift(); + popNextTestRunInfo: function () { + const url = urls.shift(); - return url; + return { + url, + testRunId: 'testRunId' + url, + }; }, get hasQueuedTestRuns () { return urls.length; }, - once: function () { - // Do nothing =) - } + once: noop, + on: noop, + + warningLog: { + copyFrom: noop, + }, }; } @@ -145,47 +138,114 @@ describe('Browser connection', function () { connection.addJob(createBrowserJobMock(['3'])); function queryStatus () { - return promisedRequest(connection.statusUrl); + return fetch(connection.statusDoneUrl); } - return promisedRequest(connection.url) - + return fetch(connection.url) .then(queryStatus) .then(function (res) { - expect(JSON.parse(res.body)).eql({ cmd: COMMAND.run, url: '1' }); + res.json() + .then((body) => { + expect(body).eql({ + cmd: COMMAND.run, + url: '1', + testRunId: 'testRunId1', + }); + }); }) .then(queryStatus) .then(function (res) { - expect(JSON.parse(res.body)).eql({ cmd: COMMAND.run, url: '2' }); + res.json() + .then((body) => { + expect(body).eql({ + cmd: COMMAND.run, + url: '2', + testRunId: 'testRunId2', + }); + }); }) .then(queryStatus) .then(function (res) { - expect(JSON.parse(res.body)).eql({ cmd: COMMAND.run, url: '3' }); + res.json() + .then((body) => { + expect(body).eql({ + cmd: COMMAND.run, + url: '3', + testRunId: 'testRunId3', + }); + }); }) .then(queryStatus) .then(function (res) { - expect(JSON.parse(res.body)).eql({ cmd: COMMAND.idle, url: connection.idleUrl }); + res.json() + .then((body) => { + expect(body).eql({ + cmd: COMMAND.idle, + url: connection.idleUrl, + testRunId: null, + }); + }); }); }); it('Should respond to the service queries with error if not ready', function () { - var testCases = [ + let testCases = [ connection.heartbeatUrl, connection.idleUrl, - connection.statusUrl + connection.statusUrl, ]; testCases = testCases.map(function (url) { - return promisedRequest(url).then(function (res) { - expect(res.statusCode).eql(500); - expect(res.body).eql('The connection is not ready yet.'); + return fetch(url).then(function (res) { + expect(res.status).eql(500); + res.text() + .then(body => { + expect(body).eql('The connection is not ready yet.'); + }); }); }); return Promise.all(testCases); }); + + it('Should set meta information for User-Agent', () => { + let eventFired = false; + + connection.on('ready', function () { + eventFired = true; + }); + + const options = { + followRedirect: false, + headers: { + 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36', + }, + }; + + const prettyUserAgentWithMetaInfo = `Chrome 41.0.2227.1 / macOS 10.10.1 (meta-info)`; + + connection.setProviderMetaInfo('meta-info', { appendToUserAgent: true }); + + return fetch(connection.url, options) + .then(() => { + expect(eventFired).to.be.true; + + expect(connection.browserInfo.userAgentProviderMetaInfo).eql(''); + expect(connection.browserInfo.parsedUserAgent.prettyUserAgent).eql(prettyUserAgentWithMetaInfo); + expect(connection.userAgent).eql(prettyUserAgentWithMetaInfo); + + // NOTE: + // set meta info after connection was already established without changing pretty user agent + connection.setProviderMetaInfo('another meta-info'); + + expect(connection.browserInfo.userAgentProviderMetaInfo).eql('another meta-info'); + expect(connection.browserInfo.parsedUserAgent.prettyUserAgent).eql(prettyUserAgentWithMetaInfo); + expect(connection.userAgent).eql(prettyUserAgentWithMetaInfo + ' (another meta-info)'); + }); + }); }); diff --git a/test/server/browser-job-test.js b/test/server/browser-job-test.js new file mode 100644 index 00000000000..d86e82c4381 --- /dev/null +++ b/test/server/browser-job-test.js @@ -0,0 +1,29 @@ +const { expect } = require('chai'); +const { noop } = require('lodash'); +const BrowserJob = require('../../lib/runner/browser-job'); + +describe('Browser Job', function () { + it('TestRunController events', function () { + const tests = [1]; + + const job = new BrowserJob({ + tests, + browserConnections: [], + proxy: null, + screenshots: null, + warningLog: null, + fixtureHookController: null, + opts: { TestRunCtor: noop }, + }); + + const testRunController = job._testRunControllerQueue[0]; + + expect(testRunController.listenerCount()).eql(6); + expect(testRunController.listenerCount('test-run-create')).eql(1); + expect(testRunController.listenerCount('test-run-ready')).eql(1); + expect(testRunController.listenerCount('test-run-restart')).eql(1); + expect(testRunController.listenerCount('test-run-before-done')).eql(1); + expect(testRunController.listenerCount('test-run-done')).eql(1); + expect(testRunController.listenerCount('test-action-done')).eql(1); + }); +}); diff --git a/test/server/browser-provider-test.js b/test/server/browser-provider-test.js index d0e6e7c1739..0e2284cc291 100644 --- a/test/server/browser-provider-test.js +++ b/test/server/browser-provider-test.js @@ -1,87 +1,209 @@ -var expect = require('chai').expect; -var Promise = require('pinkie'); -var testcafeBrowserTools = require('testcafe-browser-tools'); -var browserProviderPool = require('../../lib/browser/provider/pool'); +const expect = require('chai').expect; +const { noop, stubFalse, pick, omit } = require('lodash'); +const { nanoid } = require('nanoid'); +const { rmdirSync, statSync } = require('fs'); +const { join, dirname } = require('path'); +const proxyquire = require('proxyquire'); +const sinon = require('sinon'); +const Module = require('module'); +const dedent = require('dedent'); +const browserProviderPool = require('../../lib/browser/provider/pool'); +const BUILTIN_PROVIDERS = require('../../lib/browser/provider/built-in'); +const parseProviderName = require('../../lib/browser/provider/parse-provider-name'); +const BrowserConnection = require('../../lib/browser/connection'); +const ProviderCtor = require('../../lib/browser/provider/'); +const WARNING_MESSAGE = require('../../lib/notifications/warning-message'); +const BrowserProviderPluginHost = require('../../lib/browser/provider/plugin-host'); +const WarningLog = require('../../lib/notifications/warning-log'); + +const { browserConnectionGatewayMock } = require('./helpers/mocks'); + +class BrowserConnectionMock extends BrowserConnection { + constructor () { + const providerMock = { + openBrowser: noop, + isLocalBrowser: noop, + }; + + super(browserConnectionGatewayMock, { provider: providerMock }); + + this.ready = true; + } + + _runBrowser () { + } + + addWarning (...args) { + this.message = args[0]; + } +} describe('Browser provider', function () { + function debugMock (id) { + if (!debugMock.data) + debugMock.data = {}; + + if (!debugMock.data[id]) + debugMock.data[id] = ''; + + return newData => { + debugMock.data[id] += newData; + }; + } + + beforeEach(() => { + debugMock.data = null; + }); + describe('Path and arguments handling', function () { - var processedBrowserInfo = null; - var originalBrowserToolsGetBrowserInfo = null; - var originalBrowserToolsOpen = null; - var originalBrowserToolsGetInstallations = null; + it('Should parse the path: alias with arguments', async () => { + const browserInfo = await browserProviderPool.getBrowserInfo('path:/usr/bin/chrome --arg1 --arg2'); - function getBrowserInfo (arg) { - return browserProviderPool - .getBrowserInfo(arg) - .then(function (browserInfo) { - return browserInfo.provider.openBrowser('id', 'test-url', browserInfo.browserName); - }) - .catch(function (error) { - expect(error.message).to.contain('STOP'); - return processedBrowserInfo; + expect(browserInfo).include({ + providerName: 'path', + browserName: '/usr/bin/chrome --arg1 --arg2', + }); + }); + + it('Should parse the path: alias with arguments with spaces', async () => { + const browserInfo = await browserProviderPool.getBrowserInfo('path:`/opt/Google Chrome/chrome` --arg1 --arg2'); + + expect(browserInfo).include({ + providerName: 'path', + browserName: '`/opt/Google Chrome/chrome` --arg1 --arg2', + }); + }); + + it( 'Should get full info for all browsers', async function () { + this.timeout(10000); + + const browserInfoProperties = [ + 'provider', + 'providerName', + 'browserName', + 'browserOption', + ]; + + const browsersInfo = await browserProviderPool.getBrowserInfo('all'); + + browsersInfo.forEach( item => { + browserInfoProperties.forEach(porp => { + expect(item[porp]).exist; }); - } + } ); + } ); - before(function () { - originalBrowserToolsGetBrowserInfo = testcafeBrowserTools.getBrowserInfo; - originalBrowserToolsOpen = testcafeBrowserTools.open; - originalBrowserToolsGetInstallations = testcafeBrowserTools.getInstallations; - - testcafeBrowserTools.getBrowserInfo = function (path) { - return { - path: path, - cmd: '--internal-arg' - }; + it('Should parse the chrome: alias with arguments', async () => { + const builtInProviders = { + chrome: { isValidBrowserName: sinon.stub() }, }; - testcafeBrowserTools.open = function (browserInfo) { - processedBrowserInfo = browserInfo; + builtInProviders.chrome.isValidBrowserName + .withArgs('/usr/bin/chrome --arg1 --arg2').resolves(true); - throw new Error('STOP'); - }; + const mockedBrowserProviderPool = proxyquire('../../lib/browser/provider/pool', { + './built-in': builtInProviders, + }); - testcafeBrowserTools.getInstallations = function () { - return new Promise(function (resolve) { - resolve({ chrome: {} }); - }); + const browserInfo = await mockedBrowserProviderPool.getBrowserInfo('chrome:/usr/bin/chrome --arg1 --arg2'); + + expect(browserInfo).include({ + providerName: 'chrome', + browserName: '/usr/bin/chrome --arg1 --arg2', + }); + }); + + it('Should parse the firefox: alias with arguments', async () => { + const builtInProviders = { + firefox: { isValidBrowserName: sinon.stub() }, }; + + builtInProviders.firefox.isValidBrowserName + .withArgs('/usr/bin/firefox -arg1 -arg2').resolves(true); + + const mockedBrowserProviderPool = proxyquire('../../lib/browser/provider/pool', { + './built-in': builtInProviders, + }); + + const browserInfo = await mockedBrowserProviderPool.getBrowserInfo('firefox:/usr/bin/firefox -arg1 -arg2'); + + expect(browserInfo).include({ + providerName: 'firefox', + browserName: '/usr/bin/firefox -arg1 -arg2', + }); }); - after(function () { - testcafeBrowserTools.getBrowserInfo = originalBrowserToolsGetBrowserInfo; - testcafeBrowserTools.open = originalBrowserToolsOpen; - testcafeBrowserTools.getInstallations = originalBrowserToolsGetInstallations; + it('Should parse browser parameters with arguments', async () => { + const open = sinon.stub(); + const getBrowserInfo = sinon.stub(); + + getBrowserInfo + .withArgs('/usr/bin/chrome') + .resolves({ path: '/usr/bin/chrome', cmd: '--internal-arg' }); + + open.resolves(); + + const pathBrowserProvider = proxyquire('../../lib/browser/provider/built-in/path', { + 'testcafe-browser-tools': { open, getBrowserInfo, __esModule: false }, + }); + + await pathBrowserProvider.openBrowser('id', 'http://example.com', '/usr/bin/chrome --arg1 --arg2'); + + expect(open.callCount).equal(1); + + expect(open.args[0]).deep.equal([ + { path: '/usr/bin/chrome', cmd: '--arg1 --arg2 --internal-arg' }, + 'http://example.com', + ]); }); - it('Should parse browser parameters with arguments', function () { - return getBrowserInfo('path:/usr/bin/chrome --arg1 --arg2') - .then(function (browserInfo) { - expect(browserInfo.path).to.be.equal('/usr/bin/chrome'); - expect(browserInfo.cmd).to.be.equal('--arg1 --arg2 --internal-arg'); - }); + it('Should parse browser parameters with arguments if there are spaces in a file path', async () => { + const open = sinon.stub(); + const getBrowserInfo = sinon.stub(); + + getBrowserInfo + .withArgs('/opt/Google Chrome/chrome') + .resolves({ path: '/opt/Google Chrome/chrome', cmd: '--internal-arg' }); + + open.resolves(); + + const pathBrowserProvider = proxyquire('../../lib/browser/provider/built-in/path', { + 'testcafe-browser-tools': { open, getBrowserInfo, __esModule: false }, + }); + + await pathBrowserProvider.openBrowser('id', 'http://example.com', '`/opt/Google Chrome/chrome` --arg1 --arg2'); + + expect(open.callCount).equal(1); + + expect(open.args[0]).deep.equal([ + { path: '/opt/Google Chrome/chrome', cmd: '--arg1 --arg2 --internal-arg' }, + 'http://example.com', + ]); }); - it('Should parse browser parameters with arguments if there are spaces in a file path', function () { - return getBrowserInfo('path:`/opt/Google Chrome/chrome` --arg1 --arg2') - .then(function (browserInfo) { - expect(browserInfo.path).to.be.equal('/opt/Google Chrome/chrome'); - expect(browserInfo.cmd).to.be.equal('--arg1 --arg2 --internal-arg'); - }); + it('Should parse path and arguments for Chrome', () => { + const chromeProviderConfig = require('../../lib/browser/provider/built-in/dedicated/chrome/config'); + + expect(chromeProviderConfig('/usr/bin/chrome --arg1 --arg2')).include({ + path: '/usr/bin/chrome', + userArgs: '--arg1 --arg2', + }); }); - it('Should parse alias with arguments', function () { - return getBrowserInfo('chrome --arg1 --arg2') - .then(function (browserInfo) { - expect(browserInfo.path).to.be.equal('chrome'); - expect(browserInfo.cmd).to.contain('--arg1 --arg2 --internal-arg'); - }); + it('Should parse path and arguments for Firefox', () => { + const firefoxProviderConfig = require('../../lib/browser/provider/built-in/dedicated/firefox/config'); + + expect(firefoxProviderConfig('/usr/bin/firefox -arg1 -arg2')).include({ + path: '/usr/bin/firefox', + userArgs: '-arg1 -arg2', + }); }); }); describe('Init/dispose error handling', function () { - var initShouldSuccess = false; + let initShouldSuccess = false; - var dummyProvider = { + const dummyProvider = { init: function () { if (initShouldSuccess) return Promise.resolve(); @@ -91,7 +213,7 @@ describe('Browser provider', function () { dispose: function () { return Promise.reject(new Error('Dispose error')); - } + }, }; before(function () { @@ -133,5 +255,266 @@ describe('Browser provider', function () { }); }); }); + + describe('Browser provider module names handling', function () { + it('Should resolve short form of a scoped provider', function () { + expect(parseProviderName('@private/package')).to.deep.equal({ + providerName: '@private/package', + moduleName: '@private/testcafe-browser-provider-package', + }); + }); + + it('Should resolve long form of a scoped provider', function () { + expect(parseProviderName('@private/testcafe-browser-provider-package')).to.deep.equal({ + providerName: '@private/package', + moduleName: '@private/testcafe-browser-provider-package', + }); + }); + + it('Should resolve short form of a unscoped provider', function () { + expect(parseProviderName('package')).to.deep.equal({ + providerName: 'package', + moduleName: 'testcafe-browser-provider-package', + }); + }); + + it('Should resolve long form of a unscoped provider', function () { + expect(parseProviderName('testcafe-browser-provider-package')).to.deep.equal({ + providerName: 'package', + moduleName: 'testcafe-browser-provider-package', + }); + }); + }); + + describe('Module loading', function () { + const dummyProvider = { + init: function () { + return Promise.resolve(); + }, + + dispose: function () { + return Promise.resolve(); + }, + }; + + before(function () { + browserProviderPool.addProvider('@scope/testcafe-browser-provider-dummy', dummyProvider); + }); + + after(function () { + browserProviderPool.removeProvider('@scope/testcafe-browser-provider-dummy'); + }); + + it('Should load scoped browser provider', function () { + return browserProviderPool.getProvider('@scope/dummy').then(function (provider) { + expect(provider).to.be.not.null; + }); + }); + + it('Should load unscoped browser provider', function () { + return browserProviderPool.getProvider('chrome').then(function (provider) { + expect(provider).to.be.not.null; + }); + }); + + it('Should emit loading errors', () => { + const originResolveFilename = Module._resolveFilename; + + Module._resolveFilename = () => 'dummy-module-path.js'; + + return browserProviderPool + .getProvider('dummy') + .then(() => { + Module._resolveFilename = originResolveFilename; + + throw new Error('The error should be thrown'); + }) + .catch(err => { + Module._resolveFilename = originResolveFilename; + + expect(err.code).eql('ENOENT'); + expect(err.path).eql('dummy-module-path.js'); + }); + }); + }); + + describe('Dedicated providers base', () => { + describe('isValidBrowserName', function () { + it('Should return false if a browser is not found', () => { + const dedicatedBrowserProviderBase = proxyquire('../../lib/browser/provider/built-in/dedicated/base', { + 'testcafe-browser-tools': { + getBrowserInfo () { + return null; + }, + }, + }); + + const testProvider = Object.assign({}, dedicatedBrowserProviderBase, { + providerName: 'browser', + + getConfig () { + return {}; + }, + }); + + return testProvider + .isValidBrowserName('browser') + .then(result => { + expect(result).to.be.false; + }); + }); + + it('Should return true if a browser is found', () => { + const dedicatedBrowserProviderBase = proxyquire('../../lib/browser/provider/built-in/dedicated/base', { + 'testcafe-browser-tools': { + getBrowserInfo () { + return { alias: 'browser' }; + }, + }, + }); + + const testProvider = Object.assign({}, dedicatedBrowserProviderBase, { + providerName: 'browser', + + getConfig () { + return {}; + }, + }); + + return testProvider + .isValidBrowserName('browser') + .then(result => { + expect(result).to.be.true; + }); + }); + }); + }); + + describe('API', () => { + describe('Screenshots', () => { + it('Should add warning if provider does not support `fullPage` screenshots', () => { + const provider = new ProviderCtor({ + isLocalBrowser: () => true, + isHeadlessBrowser: () => false, + hasCustomActionForBrowser: () => false, + }); + + const bc = new BrowserConnectionMock(); + + return provider.takeScreenshot(bc.id, '', 1, 1, true) + .then(() => { + expect(bc.message).eql(WARNING_MESSAGE.screenshotsFullPageNotSupported); + }); + }); + + it('Should create a directory in screenshot was made using the plugin', () => { + const provider = new ProviderCtor({ + isLocalBrowser: stubFalse, + isHeadlessBrowser: stubFalse, + hasCustomActionForBrowser: stubFalse, + takeScreenshot: noop, + }); + + const dir = `temp${nanoid(7)}`; + const screenshotPath = join(process.cwd(), dir, 'tmp.png'); + + return provider.takeScreenshot('', screenshotPath, 0, 0, false) + .then(() => { + const stats = statSync(dirname(screenshotPath)); + + expect(stats.isDirectory()).to.be.true; + + rmdirSync(dirname(screenshotPath)); + }); + }); + }); + }); + + describe('Remote provider', () => { + it('Should log an error if a browser window was not found', async () => { + const bc = new BrowserConnectionMock(); + + bc.isReady = () => true; + + const remoteProvider = proxyquire('../../lib/browser/provider/built-in/remote', { + 'testcafe-browser-tools': { + findWindow () { + throw new Error('SomeError'); + }, + }, + debug: debugMock, + }); + + const provider = new ProviderCtor(new BrowserProviderPluginHost(remoteProvider)); + + await provider.openBrowser(bc.id); + + expect(debugMock.data['testcafe:browser:provider:built-in:remote']).eql('Error: SomeError'); + }); + }); + + describe('Features', () => { + const PROVIDERS_WITH_MULTIWINDOW_MODE = ['chrome', 'chromium', 'chrome-canary', 'edge', 'firefox']; + + it('Should support multiwindow mode in some providers', async () => { + const providers = pick(BUILTIN_PROVIDERS, PROVIDERS_WITH_MULTIWINDOW_MODE); + + for (const [name, plugin] of Object.entries(providers)) + expect(plugin.supportMultipleWindows, `Provider ${name} should support multiple windows`).ok; + }); + + it('Should not support multiwindow mode in other providers', async () => { + const providers = omit(BUILTIN_PROVIDERS, PROVIDERS_WITH_MULTIWINDOW_MODE); + + for (const [name, plugin] of Object.entries(providers)) + expect(plugin.supportMultipleWindows, `Provider ${name} should not support multiple windows`).not.ok; + }); + }); + + describe('Regression', () => { + it('Should raise a warning if a browser window was not found', async function () { + this.timeout(3000); + + const bc = new BrowserConnectionMock(); + const warningLog = new WarningLog(); + + bc.isReady = () => true; + bc.browserInfo.alias = 'chromium'; + bc.addWarning = (...args) => warningLog.addWarning(...args); + + const ProviderMock = proxyquire('../../lib/browser/provider', { + 'testcafe-browser-tools': { + default: { + findWindow () { + throw new Error('SomeError'); + }, + }, + }, + + 'os-family': { win: false, linux: false, mac: false }, + 'debug': debugMock, + }); + + const provider = new ProviderMock( + new BrowserProviderPluginHost({ + openBrowser: noop, + isLocalBrowser: () => true, + isHeadlessBrowser: () => false, + }) + ); + + await provider.openBrowser(bc.id); + + expect(debugMock.data['testcafe:browser:provider']).eql('Error: SomeError'); + expect(warningLog.messages).eql([dedent` + Could not find the "chromium" window. TestCafe is unable to resize the window or take screenshots. + + The following error occurred while TestCafe was searching for the window descriptor: + + SomeError`, + ]); + }); + }); + }); diff --git a/test/server/capturer-test.js b/test/server/capturer-test.js new file mode 100644 index 00000000000..a77d418a716 --- /dev/null +++ b/test/server/capturer-test.js @@ -0,0 +1,166 @@ +const { noop } = require('lodash'); +const { nanoid } = require('nanoid'); +const { expect } = require('chai'); +const { resolve, dirname, join } = require('path'); +const { statSync } = require('fs'); +const Capturer = require('../../lib/screenshots/capturer'); +const TestRunController = require('../../lib/runner/test-run-controller'); +const Screenshots = require('../../lib/screenshots'); +const { + writePng, + deleteFile, + readPng, +} = require('../../lib/utils/promisified-functions'); + +const WarningLog = require('../../lib/notifications/warning-log'); + + +const filePath = resolve(process.cwd(), `temp${nanoid(7)}`, 'temp.png'); + +const EMPTY_PROVIDER = { + takeScreenshot: () => noop, +}; + +const BROWSER_INFO = { + parsedUserAgent: { + os: { + name: 'os-name', + }, + }, +}; + +class CapturerMock extends Capturer { + constructor (provider) { + super(null, void 0, { + id: 'browserId', provider, + }); + } +} + +class ScreenshotsMock extends Screenshots { + constructor (options) { + super(options); + } + + createCapturerFor (test, testIndex, quarantine, connection, warningLog) { + this.capturer = super.createCapturerFor(test, testIndex, quarantine, connection, warningLog); + + this.capturer.pathPattern = { + data: { + parsedUserAgent: { + prettyUserAgent: 'user-agent', + }, + quarantineAttempt: 1, + }, + }; + + return this.capturer; + } +} + +function createScreenshotsMock () { + return new ScreenshotsMock({ + enabled: true, + path: process.cwd(), + pathPattern: '', + fullPage: false, + }); +} + +function createTestRunControllerMock (screenshots, warningLog) { + return { + _screenshots: screenshots, + test: { fixture: {}, clientScripts: [] }, + emit: noop, + _warningLog: warningLog, + _testRunCtor: function ({ browserConnection }) { + this.id = 'test-run-id'; + this.browserConnection = browserConnection; + this.initialize = noop; + }, + _proxy: { + setMode: noop, + }, + _opts: { + nativeAutomation: false, + }, + }; +} + +describe('Capturer', () => { + it('Taking screenshots does not create a directory if provider does not', async () => { + let errCode = null; + + const capturer = new CapturerMock(EMPTY_PROVIDER); + + await capturer._takeScreenshot({ filePath }); + + try { + statSync(dirname(filePath)); + } + catch (err) { + errCode = err.code; + } + + expect(errCode).eql('ENOENT'); + }); + + it('Screenshot properties for reporter', async () => { + const screenshots = createScreenshotsMock(); + const testRunControllerMock = createTestRunControllerMock(screenshots); + + await TestRunController.prototype._createTestRun.call(testRunControllerMock, { + id: 'browser-connection-id', + provider: EMPTY_PROVIDER, + browserInfo: BROWSER_INFO, + }); + + await screenshots.capturer._capture(false, { + actionId: 'action-id', + customPath: 'screenshot.png', + }); + + expect(screenshots.capturer.testEntry.screenshots[0]).eql({ + testRunId: 'test-run-id', + actionId: 'action-id', + screenshotPath: join(process.cwd(), 'screenshot.png'), + thumbnailPath: join(process.cwd(), 'thumbnails', 'screenshot.png'), + userAgent: 'user-agent', + quarantineAttempt: 1, + takenOnFail: false, + }); + }); + + it('Should not delete screenshot if unable to locate the page area', async () => { + const warningLog = new WarningLog(); + const customPath = `${nanoid(7)}screenshot.png`; + const screenshots = createScreenshotsMock(); + + const providerMock = { + takeScreenshot: async (_, path) => { + const image = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEBgIApD5fRAAAAABJRU5ErkJggg==', 'base64'); + const png = await readPng(image); + + await writePng(path, png); + }, + }; + + const testRunControllerMock = createTestRunControllerMock(screenshots, warningLog); + + await TestRunController.prototype._createTestRun.call(testRunControllerMock, { + id: 'browser-connection-id', + provider: providerMock, + browserInfo: BROWSER_INFO, + }); + + await screenshots.capturer._capture(false, { + actionId: 'action-id', + markSeed: Buffer.from([255, 255, 255, 255]), + customPath, + }); + + expect(warningLog.messages[0]).contain('Unable to locate the page area in the browser window screenshot at'); + + await deleteFile(customPath); + }); +}); diff --git a/test/server/chrome-provider-config-test.js b/test/server/chrome-provider-config-test.js index ca6ec67806a..b143fc443fb 100644 --- a/test/server/chrome-provider-config-test.js +++ b/test/server/chrome-provider-config-test.js @@ -1,16 +1,18 @@ -var expect = require('chai').expect; -var OS = require('os-family'); -var getChromeConfig = require('../../lib/browser/provider/built-in/chrome/config.js'); +const expect = require('chai').expect; +const osFamily = require('os-family'); +const getChromeConfig = require('../../lib/browser/provider/built-in/dedicated/chrome/config.js'); describe('Chrome provider config parser', function () { it('Should parse options and arguments', function () { - var config = getChromeConfig('/chrome/path/with\\::headless:emulation:device=iPhone 4;cdpPort=9222 --arg1 --arg2'); + const config = getChromeConfig('/chrome/path/with\\::headless:emulation:device=Apple iPhone 4;cdpPort=9222 --arg1 --arg2'); expect(config.path).to.equal('/chrome/path/with:'); + expect(config.userProfile).to.be.false; expect(config.headless).to.be.true; expect(config.emulation).to.be.true; + expect(config.deviceName).to.equal('Apple iPhone 4'); expect(config.mobile).to.be.true; expect(config.touch).to.be.true; expect(config.width).to.equal(320); @@ -20,7 +22,7 @@ describe('Chrome provider config parser', function () { expect(config.userAgent).to.equal([ 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us)', 'AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148', - 'Safari/6533.18.5' + 'Safari/6533.18.5', ].join(' ')); expect(config.cdpPort).to.equal('9222'); @@ -28,10 +30,11 @@ describe('Chrome provider config parser', function () { }); it('Should parse custom device configuration', function () { - var config = getChromeConfig('emulation:userAgent=Mozilla/XX\\; Browser/XX.XX.XX;width=800;height=600;scaleFactor=1;touch=false;mobile=true'); + const config = getChromeConfig('emulation:userAgent=Mozilla/XX\\; Browser/XX.XX.XX;width=800;height=600;scaleFactor=1;touch=false;mobile=true'); expect(config.emulation).to.be.true; + expect(config.deviceName).to.be.undefined; expect(config.mobile).to.be.true; expect(config.touch).to.be.false; expect(config.width).to.equal(800); @@ -41,10 +44,11 @@ describe('Chrome provider config parser', function () { }); it('Should provide default values for emulation options', function () { - var config = getChromeConfig('emulation'); + const config = getChromeConfig('emulation'); expect(config.emulation).to.be.true; + expect(config.deviceName).to.be.undefined; expect(config.mobile).to.be.false; expect(config.touch).to.be.undefined; expect(config.width).to.equal(0); @@ -53,11 +57,12 @@ describe('Chrome provider config parser', function () { }); it('Should provide default values for emulation options in headless mode', function () { - var config = getChromeConfig('headless'); + const config = getChromeConfig('headless'); expect(config.headless).to.be.true; expect(config.emulation).to.be.true; + expect(config.deviceName).to.be.undefined; expect(config.mobile).to.be.false; expect(config.touch).to.be.undefined; expect(config.width).to.equal(1280); @@ -65,9 +70,19 @@ describe('Chrome provider config parser', function () { expect(config.scaleFactor).to.equal(0); }); - if (OS.win) { + it('Should support userProfile mode', function () { + let config = getChromeConfig('userProfile'); + + expect(config.userProfile).to.be.true; + + config = getChromeConfig('--user-data-dir=/dev/null'); + + expect(config.userProfile).to.be.true; + }); + + if (osFamily.win) { it('Should allow unescaped colon as disk/path separator on Windows', function () { - var config = getChromeConfig('C:\\Chrome\\chrome.exe:headless'); + const config = getChromeConfig('C:\\Chrome\\chrome.exe:headless'); expect(config.path).to.eql('C:\\Chrome\\chrome.exe'); expect(config.headless).to.be.true; diff --git a/test/server/cli-argument-parser-test.js b/test/server/cli-argument-parser-test.js index 9223062866b..0879a28085b 100644 --- a/test/server/cli-argument-parser-test.js +++ b/test/server/cli-argument-parser-test.js @@ -1,8 +1,14 @@ -var expect = require('chai').expect; -var path = require('path'); -var fs = require('fs'); -var tmp = require('tmp'); -var CliArgumentParser = require('../../lib/cli/argument-parser'); +const { expect } = require('chai'); +const path = require('path'); +const fs = require('fs'); +const tmp = require('tmp'); +const { find } = require('lodash'); +const CliArgumentParser = require('../../lib/cli/argument-parser'); +const { nanoid } = require('nanoid'); +const runOptionNames = require('../../lib/configuration/run-option-names'); +const shouldMoveOptionToEnd = require('../../lib/cli/utils/should-move-option-to-end'); +const QUARANTINE_OPTION_NAMES = require('../../lib/configuration/quarantine-option-names'); +const { SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES } = require('../../lib/configuration/skip-js-errors-option-names'); describe('CLI argument parser', function () { this.timeout(10000); @@ -10,9 +16,9 @@ describe('CLI argument parser', function () { tmp.setGracefulCleanup(); function parse (args, cwd) { - var parser = new CliArgumentParser(cwd); + const parser = new CliArgumentParser(cwd); - args = ['node', 'index.js'].concat(typeof args === 'string' ? args.split(/\s+/) : args); + args = ['node', 'testcafe'].concat(typeof args === 'string' ? args.split(/\s+/) : args); return parser.parse(args) .then(function () { @@ -30,35 +36,69 @@ describe('CLI argument parser', function () { }); } + describe('Set browser provider name', function () { + it('Should set the default provider name to "locally-installed" from "--list-browsers"', function () { + return parse('--list-browsers') + .then(function (parser) { + expect(parser.opts.listBrowsers).eql(true); + expect(parser.opts.providerName).eql('locally-installed'); + }); + }); + + it('Should parse the browser provider name from "--list-browsers saucelabs"', function () { + return parse('--list-browsers saucelabs') + .then(function (parser) { + expect(parser.opts.listBrowsers).eql(true); + expect(parser.opts.providerName).eql('saucelabs'); + }); + }); + + it('Should set the default provider name to "locally-installed" from "-b"', function () { + return parse('-b') + .then(function (parser) { + expect(parser.opts.listBrowsers).eql(true); + expect(parser.opts.providerName).eql('locally-installed'); + }); + }); + + it('Should parse "-b saucelabs" browser provider name from "-b saucelabs"', function () { + return parse('-b saucelabs') + .then(function (parser) { + expect(parser.opts.listBrowsers).eql(true); + expect(parser.opts.providerName).eql('saucelabs'); + }); + }); + }); + describe('Browser list', function () { it('Should be parsed as array of aliases or paths', function () { - return parse('path:"/Applications/Firefox.app",ie,chrome,firefox,') + return parse('path:"/Applications/Firefox.app",edge,chrome,firefox,') .then(function (parser) { - expect(parser.browsers).eql(['path:/Applications/Firefox.app', 'ie', 'chrome', 'firefox']); + expect(parser.opts.browsers).eql(['path:/Applications/Firefox.app', 'edge', 'chrome', 'firefox']); }); }); it('Should accept "remote" alias', function () { - return parse('remote:12,ie,remote,chrome,remote:3') + return parse('remote:12,edge,remote,chrome,remote:3') .then(function (parser) { - expect(parser.browsers).eql(['ie', 'chrome']); + expect(parser.opts.browsers).eql(['edge', 'chrome']); expect(parser.remoteCount).eql(16); }); }); it('Should accept "all" alias', function () { - return parse('ie,chrome,all') + return parse('edge,chrome,all') .then(function (parser) { - expect(parser.browsers).eql(['ie', 'chrome', 'all']); + expect(parser.opts.browsers).eql(['edge', 'chrome', 'all']); }); }); it('Should split browsers correctly if paths have commas and quotes', function () { - return parse('path:"/Apps,Libs/\'Firefox.app",ie,chrome,firefox,path:\'/Apps,Libs/"Chrome.app\'') + return parse('path:"/Apps,Libs/\'Firefox.app",edge,chrome,firefox,path:\'/Apps,Libs/"Chrome.app\'') .then(function (parser) { - expect(parser.browsers).eql([ - 'path:/Apps,Libs/\'Firefox.app', 'ie', 'chrome', 'firefox', - 'path:/Apps,Libs/"Chrome.app' + expect(parser.opts.browsers).eql([ + 'path:/Apps,Libs/\'Firefox.app', 'edge', 'chrome', 'firefox', + 'path:/Apps,Libs/"Chrome.app', ]); }); }); @@ -66,9 +106,9 @@ describe('CLI argument parser', function () { it('Should split browsers correctly if providers have arguments', function () { return parse(['path:"/Apps/Firefox.app --arg1",chrome --arg2']) .then(function (parser) { - expect(parser.browsers).eql([ + expect(parser.opts.browsers).eql([ 'path:/Apps/Firefox.app --arg1', - 'chrome --arg2' + 'chrome --arg2', ]); }); }); @@ -83,11 +123,11 @@ describe('CLI argument parser', function () { }); it('Should raise error if "--ports" option value is not a integer', function () { - return assertRaisesError('--ports 1337,yo', 'Port number is expected to be a non-negative number, but it was "yo".'); + return assertRaisesError('--ports 1337,yo', 'The port number ("yo") is not of expected type (non-negative number).'); }); it('Should raise error if "--ports" option has less than 2 ports specified', function () { - return assertRaisesError('--ports 1337', 'The "--ports" option requires two numbers to be specified.'); + return assertRaisesError('--ports 1337', 'The "--ports" option requires two arguments.'); }); }); @@ -100,7 +140,7 @@ describe('CLI argument parser', function () { }); it('Should raise an error if the "--selector-timeout" option value is not an integer', function () { - return assertRaisesError('--selector-timeout yo', 'Selector timeout is expected to be a non-negative number, but it was "yo".'); + return assertRaisesError('--selector-timeout yo', 'The Selector timeout ("yo") is not of expected type (non-negative number).'); }); }); @@ -113,12 +153,90 @@ describe('CLI argument parser', function () { }); it('Should raise an error if the "--assertion-timeout" option value is not an integer', function () { - return assertRaisesError('--assertion-timeout yo', 'Assertion timeout is expected to be a non-negative number, but it was "yo".'); + return assertRaisesError('--assertion-timeout yo', 'The assertion timeout ("yo") is not of expected type (non-negative number).'); + }); + }); + + describe('Page load timeout', function () { + it('Should parse "--page-load-timeout" option as integer value', function () { + return parse('--page-load-timeout 1000') + .then(function (parser) { + expect(parser.opts.pageLoadTimeout).eql(1000); + }); + }); + + it('Should raise an error if the "--page-load-timeout" option value is not an integer', function () { + return assertRaisesError('--page-load-timeout yo', 'The page load timeout ("yo") is not of expected type (non-negative number).'); + }); + }); + + describe('Request timeout', () => { + describe('Page request timeout', () => { + it('Should parse the option as integer value', async () => { + const parser = await parse('--page-request-timeout 1000'); + + expect(parser.opts.pageRequestTimeout).eql(1000); + }); + + it('Should raise an error on invalid option value', () => { + return assertRaisesError('--page-request-timeout str', 'The page request timeout ("str") is not of expected type (non-negative number).'); + }); + }); + + describe('Ajax request timeout', () => { + it('Should parse the option as integer value', async () => { + const parser = await parse('--ajax-request-timeout 1000'); + + expect(parser.opts.ajaxRequestTimeout).eql(1000); + }); + + it('Should raise an error on invalid option value', () => { + return assertRaisesError('--ajax-request-timeout str', 'The AJAX request timeout ("str") is not of expected type (non-negative number).'); + }); + }); + }); + + describe('Browser initialization timeout', function () { + it('Should parse "--browser-init-timeout" option as integer value', function () { + return parse('--browser-init-timeout 1000') + .then(function (parser) { + expect(parser.opts.browserInitTimeout).eql(1000); + }); + }); + + it('Should raise an error if the "--browser-init-timeout" option value is not an integer', function () { + return assertRaisesError('--browser-init-timeout yo', 'The browser initialization timeout ("yo") is not of expected type (non-negative number).'); + }); + }); + + describe('Test execution timeout', function () { + it('Should parse "--test-execution-timeout" option as integer value', function () { + return parse('--test-execution-timeout 1000') + .then(function (parser) { + expect(parser.opts.testExecutionTimeout).eql(1000); + }); + }); + + it('Should raise an error if the "--test-execution-timeout" option value is not an integer', function () { + return assertRaisesError('--test-execution-timeout yo', 'The test execution timeout ("yo") is not of expected type (non-negative number).'); + }); + }); + + describe('Run execution timeout', function () { + it('Should parse "--run-execution-timeout" option as integer value', function () { + return parse('--run-execution-timeout 1000') + .then(function (parser) { + expect(parser.opts.runExecutionTimeout).eql(1000); + }); + }); + + it('Should raise an error if the "--run-execution-timeout" option value is not an integer', function () { + return assertRaisesError('--run-execution-timeout yo', 'The run execution timeout ("yo") is not of expected type (non-negative number).'); }); }); describe('Speed', function () { - it('Should parse "--speed" option as a number ', function () { + it('Should parse "--speed" option as a number', function () { return parse('--speed 0.01') .then(function (parser) { expect(parser.opts.speed).eql(0.01); @@ -126,6 +244,22 @@ describe('CLI argument parser', function () { }); }); + describe('Concurrency', function () { + it('Should parse "--concurrency" option as a number', function () { + return parse('--concurrency 2') + .then(function (parser) { + expect(parser.opts.concurrency).eql(2); + }); + }); + + it('Should parse "-c" option as a number', function () { + return parse('-c 2') + .then(function (parser) { + expect(parser.opts.concurrency).eql(2); + }); + }); + }); + describe('App initialization delay', function () { it('Should parse "--app-init-delay" option as integer value', function () { return parse('--app-init-delay 1000') @@ -135,7 +269,16 @@ describe('CLI argument parser', function () { }); it('Should raise an error if the "--app-init-delay" option value is not an integer', function () { - return assertRaisesError('--app-init-delay yo', 'Tested app initialization delay is expected to be a non-negative number, but it was "yo".'); + return assertRaisesError('--app-init-delay yo', 'The tested app initialization delay ("yo") is not of expected type (non-negative number).'); + }); + }); + + describe('Node arguments', function () { + it('Should parse node flags', function () { + return parse('chrome test 1 --inspect-brk flag') + .then(function (parser) { + expect(parser.opts.v8Flags).to.deep.equal(['--inspect-brk']); + }); }); }); @@ -143,168 +286,679 @@ describe('CLI argument parser', function () { it('Should filter by test name with "-t, --test" option', function () { return parse('-t test.js') .then(function (parser) { - expect(parser.filter('test.js')).to.be.true; - expect(parser.filter('1test.js')).to.be.false; - expect(parser.filter('test-js')).to.be.false; + expect(parser.opts.filter('test.js')).to.be.true; + expect(parser.opts.filter('1test.js')).to.be.false; + expect(parser.opts.filter('test-js')).to.be.false; }); }); it('Should filter by test name with "-T, --test-grep" option', function () { parse('-T ^test\\d+$') .then(function (parser) { - expect(parser.filter('test1')).to.be.true; - expect(parser.filter('test2')).to.be.true; - expect(parser.filter('test')).to.be.false; + expect(parser.opts.filter.testGrep.test('test1')).to.be.true; + expect(parser.opts.filter.testGrep.test('test')).to.be.false; + + expect(parser.opts.filter('test1')).to.be.true; + expect(parser.opts.filter('test2')).to.be.true; + expect(parser.opts.filter('test')).to.be.false; }); }); it('Should raise error if "-T, --test-grep" value is invalid regular expression', function () { - return assertRaisesError('-T *+', 'The "--test-grep" option value is not a valid regular expression.'); + return assertRaisesError('-T *+', 'The "--test-grep" option does not contain a valid regular expression.'); }); it('Should filter by fixture name with "-f, --fixture" option', function () { return parse('-f fixture.js') .then(function (parser) { - expect(parser.filter('test', 'fixture.js')).to.be.true; - expect(parser.filter('test', '1fixture.js')).to.be.false; - expect(parser.filter('test', 'fixture-js')).to.be.false; + expect(parser.opts.filter('test', 'fixture.js')).to.be.true; + expect(parser.opts.filter('test', '1fixture.js')).to.be.false; + expect(parser.opts.filter('test', 'fixture-js')).to.be.false; }); }); it('Should filter by fixture name with "-F, --fixture-grep" option', function () { return parse('-F ^fixture\\d+$') .then(function (parser) { - expect(parser.filter('test', 'fixture1')).to.be.true; - expect(parser.filter('test', 'fixture2')).to.be.true; - expect(parser.filter('test', 'fixture')).to.be.false; + expect(parser.opts.filter.fixtureGrep.test('fixture1')).to.be.true; + expect(parser.opts.filter.fixtureGrep.test('fixture')).to.be.false; + + expect(parser.opts.filter('test', 'fixture1')).to.be.true; + expect(parser.opts.filter('test', 'fixture2')).to.be.true; + expect(parser.opts.filter('test', 'fixture')).to.be.false; }); }); it('Should raise error if "-F, --fixture-grep" value is invalid regular expression', function () { - return assertRaisesError('-F *+', 'The "--fixture-grep" option value is not a valid regular expression.'); + return assertRaisesError('-F *+', 'The "--fixture-grep" option does not contain a valid regular expression.'); + }); + + it('Should filter by test meta with "--test-meta" option', function () { + return parse('--test-meta meta=test') + .then(function (parser) { + expect(parser.opts.filter.testMeta).to.be.deep.equal({ meta: 'test' }); + + expect(parser.opts.filter(null, null, null, { meta: 'test' })).to.be.true; + expect(parser.opts.filter(null, null, null, { another: 'meta', meta: 'test' })).to.be.true; + expect(parser.opts.filter(null, null, null, {})).to.be.false; + expect(parser.opts.filter(null, null, null, { meta: 'notest' })).to.be.false; + }); + }); + + it('Should filter by fixture meta with "--fixture-meta" option', function () { + return parse('--fixture-meta meta=test,more=meta') + .then(function (parser) { + expect(parser.opts.fixtureMeta).to.be.deep.equal({ meta: 'test', more: 'meta' }); + + expect(parser.opts.filter(null, null, null, null, { meta: 'test', more: 'meta' })).to.be.true; + expect(parser.opts.filter(null, null, null, null, { another: 'meta', meta: 'test', more: 'meta' })).to.be.true; + expect(parser.opts.filter(null, null, null, null, {})).to.be.false; + expect(parser.opts.filter(null, null, null, null, { meta: 'test' })).to.be.false; + expect(parser.opts.filter(null, null, null, null, { meta: 'test', more: 'another' })).to.be.false; + }); + }); + + it('Should throw an error if invalid meta is specified', () => { + return parse('--fixture-meta meta') + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(function (error) { + expect(error.message).contains('The "--fixture-meta" option value is not a valid key-value pair.'); + + return parse('--fixture-meta =test'); + }) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(function (error) { + expect(error.message).contains('The "--fixture-meta" option value is not a valid key-value pair.'); + + return parse('--test-meta meta'); + }) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(function (error) { + expect(error.message).contains('The "--test-meta" option value is not a valid key-value pair.'); + + return parse('--test-meta =test'); + }) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(function (error) { + expect(error.message).contains('The "--test-meta" option value is not a valid key-value pair.'); + }); + }); + + it('Should raise error if "--test-meta" value is invalid json', function () { + return assertRaisesError('--test-meta error', 'The "--test-meta" option does not contain a valid key-value pair.'); + }); + + it('Should raise error if "--fixture-meta" value is invalid json', function () { + return assertRaisesError('--fixture-meta error', 'The "--fixture-meta" option does not contain a valid key-value pair.'); }); it('Should combine filters provided by multiple options', function () { return parse('-t thetest1 -T test\\d+$') .then(function (parser) { - expect(parser.filter('thetest1')).to.be.true; - expect(parser.filter('thetest2')).to.be.false; + expect(parser.opts.filter('thetest1')).to.be.true; + expect(parser.opts.filter('thetest2')).to.be.false; }) .then(function () { return parse('-t thetest1 -T test$ '); }) .then(function (parser) { - expect(parser.filter('thetest1')).to.be.false; - expect(parser.filter('thetest')).to.be.false; + expect(parser.opts.filter('thetest1')).to.be.false; + expect(parser.opts.filter('thetest')).to.be.false; }) .then(function () { return parse('-f thefixture1 -F fixture\\d+$'); }) .then(function (parser) { - expect(parser.filter(null, 'thefixture1')).to.be.true; - expect(parser.filter(null, 'thefixture2')).to.be.false; + expect(parser.opts.filter(null, 'thefixture1')).to.be.true; + expect(parser.opts.filter(null, 'thefixture2')).to.be.false; }) .then(function () { return parse('-f thefixture1 -F fixture$'); }) .then(function (parser) { - expect(parser.filter(null, 'thefixture1')).to.be.false; - expect(parser.filter(null, 'thefixture')).to.be.false; + expect(parser.opts.filter(null, 'thefixture1')).to.be.false; + expect(parser.opts.filter(null, 'thefixture')).to.be.false; }) .then(function () { return parse('-t thetest1 -f thefixture1'); }) .then(function (parser) { - expect(parser.filter('thetest1', 'thefixture1')).to.be.true; - expect(parser.filter('thetest', 'thefixture1')).to.be.false; - expect(parser.filter('thetest1', 'thefixture')).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture1')).to.be.true; + expect(parser.opts.filter('thetest', 'thefixture1')).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture')).to.be.false; }) .then(function () { return parse('-T test\\d+$ -f thefixture1 -F fixture\\d+$'); }) .then(function (parser) { - expect(parser.filter('thetest1', 'thefixture1')).to.be.true; - expect(parser.filter('thetest', 'thefixture1')).to.be.false; - expect(parser.filter('thetest1', 'thefixture')).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture1')).to.be.true; + expect(parser.opts.filter('thetest', 'thefixture1')).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture')).to.be.false; + }) + .then(function () { + return parse('-t thetest1 --test-meta meta=test'); + }) + .then(function (parser) { + expect(parser.opts.filter('thetest1', null, null, { meta: 'test' })).to.be.true; + expect(parser.opts.filter('thetest1', null, null, {})).to.be.false; + expect(parser.opts.filter('thetest2', null, null, { meta: 'test' })).to.be.false; + }) + .then(function () { + return parse('-f thefixture1 --test-meta meta=test'); + }) + .then(function (parser) { + expect(parser.opts.filter(null, 'thefixture1', null, { meta: 'test' })).to.be.true; + expect(parser.opts.filter(null, 'thefixture1', null, {})).to.be.false; + expect(parser.opts.filter(null, 'thefixture2', null, { meta: 'test' })).to.be.false; + }) + .then(function () { + return parse('-t thetest1 -f thefixture1 --test-meta meta=test'); + }) + .then(function (parser) { + expect(parser.opts.filter('thetest1', 'thefixture1', null, { meta: 'test' })).to.be.true; + expect(parser.opts.filter('thetest1', 'thefixture1', null, {})).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture2', null, { meta: 'test' })).to.be.false; + expect(parser.opts.filter('thetest2', 'thefixture1', null, { meta: 'test' })).to.be.false; + }) + .then(function () { + return parse('-t thetest1 --fixture-meta meta=test'); + }) + .then(function (parser) { + expect(parser.opts.filter('thetest1', null, null, null, { meta: 'test' })).to.be.true; + expect(parser.opts.filter('thetest1', null, null, null, {})).to.be.false; + expect(parser.opts.filter('thetest2', null, null, null, { meta: 'test' })).to.be.false; + }) + .then(function () { + return parse('-f thefixture1 --fixture-meta meta=test'); + }) + .then(function (parser) { + expect(parser.opts.filter(null, 'thefixture1', null, null, { meta: 'test' })).to.be.true; + expect(parser.opts.filter(null, 'thefixture1', null, null, {})).to.be.false; + expect(parser.opts.filter(null, 'thefixture2', null, null, { meta: 'test' })).to.be.false; + }) + .then(function () { + return parse('-t thetest1 -f thefixture1 --fixture-meta meta=test'); + }) + .then(function (parser) { + expect(parser.opts.filter('thetest1', 'thefixture1', null, null, { meta: 'test' })).to.be.true; + expect(parser.opts.filter('thetest1', 'thefixture1', null, null, {})).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture2', null, null, { meta: 'test' })).to.be.false; + expect(parser.opts.filter('thetest2', 'thefixture1', null, null, { meta: 'test' })).to.be.false; + }) + .then(function () { + return parse('-t thetest1 -f thefixture1 --test-meta test=test --fixture-meta fixture=test'); + }) + .then(function (parser) { + expect(parser.opts.filter('thetest1', 'thefixture1', null, { test: 'test' }, { fixture: 'test' })).to.be.true; + expect(parser.opts.filter('thetest1', 'thefixture1', null, {}, { fixture: 'test' })).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture1', null, { test: 'test' }, {})).to.be.false; + expect(parser.opts.filter('thetest1', 'thefixture2', null, { test: 'test' }, { fixture: 'test' })).to.be.false; + expect(parser.opts.filter('thetest2', 'thefixture1', null, { test: 'test' }, { fixture: 'test' })).to.be.false; }); }); - }); - it('Should accept globs and paths as source files', function () { - var cwd = process.cwd(); - - var expected = [ - 'test/server/data/file-list/file-1.js', - 'test/server/data/file-list/file-2.js', - 'test/server/data/file-list/dir1/dir1-1/file-1-1-1.js', - 'test/server/data/file-list/dir1/file-1-1.js', - 'test/server/data/file-list/dir1/file-1-2.js', - 'test/server/data/file-list/dir1/file-1-3.testcafe', - 'test/server/data/file-list/dir1/file-1-4.ts', - 'test/server/data/file-list/dir2/file-2-2.js', - 'test/server/data/file-list/dir2/file-2-3.js' - ]; + it("'.filter' property should equal undefined if filtering options are not provided", () => { + return parse('param1') + .then(parser => { + expect(parser.filter).is.undefined; + }); + }); + }); - expected = expected.map(function (file) { - return path.resolve(cwd, file); + describe('Ssl options', () => { + it('Should parse ssl options', () => { + return parse('param1 --ssl passphrase=sample;sessionTimeout=1000;rejectUnauthorized=false;=onlyValue;onlyKey=') + .then(parser => { + expect(parser.opts.ssl.passphrase).eql('sample'); + expect(parser.opts.ssl.sessionTimeout).eql(1000); + expect(parser.opts.ssl.rejectUnauthorized).eql(false); + expect(parser.opts.ssl.onlyKey).to.be.undefined; + }); }); - return parse('chrome ' + - 'test/server/data/file-list/file-1.js ' + - path.join(cwd, 'test/server/data/file-list/file-2.js') + ' ' + - 'test/server/data/file-list/dir1 ' + - 'test/server/data/file-list/dir2/*.js ' + - '!test/server/data/file-list/dir2/file-2-1.js ' + - 'test/server/data/file-list/dir3') - .then(function (parser) { - expect(parser.src).eql(expected); + describe('`key`, `cert` and `pfx` keys', () => { + it('Should parse keys as file paths and read its content', () => { + const keyFile = tmp.fileSync(); + const certFile = tmp.fileSync(); + const pfxFile = tmp.fileSync(); + const keyFileContent = Buffer.from(nanoid()); + const certFileContent = Buffer.from(nanoid()); + const pfxFileContent = Buffer.from(nanoid()); + + fs.writeFileSync(keyFile.name, keyFileContent); + fs.writeFileSync(certFile.name, certFileContent); + fs.writeFileSync(pfxFile.name, pfxFileContent); + + return parse(`--ssl key=${keyFile.name};cert=${certFile.name};pfx=${pfxFile.name}`) + .then(parser => { + expect(parser.opts.ssl.key).deep.eql(keyFileContent); + expect(parser.opts.ssl.cert).deep.eql(certFileContent); + expect(parser.opts.ssl.pfx).deep.eql(pfxFileContent); + }); + }); + + it('Should not read file content if file does not exists', () => { + const dummyFilePath = '/dummy-file-path'; + + return parse(`--ssl key=${dummyFilePath}`) + .then(parser => { + expect(parser.opts.ssl.key).eql(dummyFilePath); + }); + }); + + it('Should interpret a long path as a certificate content', () => { + const keyFileContent = nanoid(5000); + + return parse(`--ssl key=${keyFileContent}`) + .then(parser => { + expect(parser.opts.ssl.key).eql(keyFileContent); + }); + }); + + it('Should throw an error if a file is not readable', () => { + return parse(`--ssl key=${__dirname}`) + .catch(error => { + expect(error.message).to.include( + `Unable to read the file referenced by the "key" ssl option ("${__dirname}"). Error details:` + ); + }) + .then(() => parse(`--ssl cert=${__dirname}`)) + .catch(error => { + expect(error.message).to.include( + `Unable to read the file referenced by the "cert" ssl option ("${__dirname}"). Error details:` + ); + }) + .then(() => parse(`--ssl pfx=${__dirname}`)) + .catch(error => { + expect(error.message).to.include( + `Unable to read the file referenced by the "pfx" ssl option ("${__dirname}"). Error details:` + ); + }); }); + }); }); - it('Should use "test" and "tests" dirs if source files are not specified', function () { - var workingDir = path.join(__dirname, './data/file-list'); + describe('Video options', () => { + it('Should parse video recording options', () => { + return parse(`--video /home/user/video --video-options singleFile=true,failedOnly --video-encoding-options c:v=x264`) + .then(parser => { + expect(parser.opts.video).eql('/home/user/video'); + expect(parser.opts.videoOptions.singleFile).eql(true); + expect(parser.opts.videoOptions.failedOnly).eql(true); + expect(parser.opts.videoEncodingOptions['c:v']).eql('x264'); + }); + }); + + it('Should provide "undefined" as a default value for video recording options', () => { + return parse(``) + .then(parser => { + expect(parser.opts.video).eql(void 0); + expect(parser.opts.videoOptions).eql(void 0); + expect(parser.opts.videoEncodingOptions).eql(void 0); + }); + }); + }); - var expected = [ - 'test/test-dir-file.js', - 'tests/tests-dir-file.js' - ]; + describe('Screenshot options', () => { + it('Should parse screenshot options', async () => { + const parser = await parse('--screenshots path=/a/b/c,fullPage=true,takeOnFails=true,pathPattern=${TEST}.png'); - expected = expected.map(function (file) { - return path.resolve(workingDir, file); + expect(parser.opts.screenshots.takeOnFails).to.be.ok; + expect(parser.opts.screenshots.path).equal('/a/b/c'); + expect(parser.opts.screenshots.fullPage).to.be.ok; + expect(parser.opts.screenshots.pathPattern).equal('${TEST}.png'); }); - return parse('chrome', workingDir) - .then(function (parser) { - expect(parser.src).eql(expected); + it('Should understand legacy keys', async () => { + const parser = await parse('--screenshots-on-fails --screenshots /a/b/c --screenshot-path-pattern ${TEST}.png'); + + expect(parser.opts.screenshots.takeOnFails).to.be.ok; + expect(parser.opts.screenshots.path).equal('/a/b/c'); + expect(parser.opts.screenshots.fullPage).to.be.undefined; + expect(parser.opts.screenshots.pathPattern).equal('${TEST}.png'); + }); + + it('Should prioritize over legacy keys', async () => { + const parser = await parse('--screenshots path=/a/b/c,takeOnFails=false,pathPattern=${TEST}.png --screenshots-on-fails --screenshot-path-pattern not${TEST}.png'); + + expect(parser.opts.screenshots.takeOnFails).to.be.false; + expect(parser.opts.screenshots.path).equal('/a/b/c'); + expect(parser.opts.screenshots.fullPage).to.be.undefined; + expect(parser.opts.screenshots.pathPattern).equal('${TEST}.png'); + }); + }); + + describe('Compiler options', () => { + it('Basic', async () => { + const cmd = '--compiler-options ' + + 'typescript.options.skipLibCheck=true;' + + "typescript.options.lib=ES5,'WebWorker';" + + 'typescript.configPath=/path-to-tsconfig.json'; + + const parser = await parse(cmd); + + const typescriptCompilerOptions = parser.opts.compilerOptions.typescript; + + expect(typescriptCompilerOptions.options.skipLibCheck).eql(true); + expect(typescriptCompilerOptions.options.lib).eql(['ES5', 'WebWorker']); + expect(typescriptCompilerOptions.configPath).eql('/path-to-tsconfig.json'); + }); + + it('Array option with a single element', async () => { + const parser = await parse('--compiler-options typescript.options.lib=ES5'); + + expect(parser.opts.compilerOptions.typescript.options.lib).eql(['ES5']); + }); + }); + + it('Client scripts', () => { + return parse('--client-scripts asserts/jquery.js,mockDate.js') + .then(parser => { + expect(parser.opts.clientScripts).eql([ + 'asserts/jquery.js', + 'mockDate.js', + ]); }); }); - it('Should parse the screenshot path and ensure it exists', function () { - var dir = path.join(tmp.dirSync().name, 'my/screenshots'); + it('Should parse reporters and their output file paths and ensure they exist', function () { + const cwd = process.cwd(); + const filePath = path.join(tmp.dirSync().name, 'my/reports/report.json'); - return parse('-s ' + dir) + return parse('-r list,json:' + filePath) .then(function (parser) { - expect(parser.opts.screenshots).eql(dir); - expect(fs.existsSync(dir)).to.be.true; + expect(parser.opts.reporter[0].name).eql('list'); + expect(parser.opts.reporter[0].output).to.be.undefined; + expect(parser.opts.reporter[1].name).eql('json'); + expect(parser.opts.reporter[1].output).eql(path.resolve(cwd, filePath)); }); }); + it('Should parse bool false value for booleanOrObject options', async () => { + async function checkCliArgs (argsString) { + const parser = await parse(argsString); + + expect(parser.opts.quarantineMode).equal(false); + expect(parser.opts.skipJsErrors).equal(false); + } + + await checkCliArgs('chrome -q false test.js -e false'); + }); + + it('Should move booleanOrObject option to the end in no value provided', async () => { + const quarantineOptions = Object.values(QUARANTINE_OPTION_NAMES); + const skipJsErrorsOptions = Object.values(SKIP_JS_ERRORS_OPTIONS_OBJECT_OPTION_NAMES); + + function checkOption (args, optionName, subOptions) { + const optionIndex = args.indexOf(optionName); + + return shouldMoveOptionToEnd(args, optionIndex, subOptions); + } + + expect(checkOption(['-q', 'attemptLimit=5', 'chrome'], '-q', quarantineOptions)).eql(false); + expect(checkOption(['-e', 'message=test', 'chrome'], '-e', skipJsErrorsOptions)).eql(false); + expect(checkOption(['-q', 'chrome', 'test.js'], '-q', quarantineOptions)).eql(true); + expect(checkOption(['-e', 'chrome', 'test.js'], '-e', skipJsErrorsOptions)).eql(true); + }); + + describe('Quarantine Option', function () { + it('Should parse quarantine arguments', async () => { + async function checkCliArgs (argsString) { + const parser = await parse(argsString); + + expect(parser.opts.quarantineMode).to.be.ok; + expect(parser.opts.quarantineMode.attemptLimit).equal(5); + expect(parser.opts.quarantineMode.successThreshold).equal(1); + } + + await checkCliArgs('-q attemptLimit=5,successThreshold=1'); + await checkCliArgs('--quarantine-mode attemptLimit=5,successThreshold=1'); + }); + + it('Should pass if only "successThreshold" is provided', async () => { + async function checkCliArgs (argsString) { + const parser = await parse(argsString); + + expect(parser.opts.quarantineMode).to.be.ok; + expect(parser.opts.quarantineMode.successThreshold).equal(1); + } + + await checkCliArgs('-q successThreshold=1'); + await checkCliArgs('--quarantine-mode successThreshold=1'); + }); + + it('Should fail if the argument value is not specified', async () => { + await assertRaisesError('-q attemptLimit=', 'The "--quarantine-mode" option does not contain a valid key-value pair.'); + await assertRaisesError('--quarantine-mode attemptLimit=', 'The "--quarantine-mode" option does not contain a valid key-value pair.'); + }); + + it('Should fail if "successThreshold" is greater or equal to "attemptLimit"', async () => { + await assertRaisesError('-q attemptLimit=2,successThreshold=2', 'The value of "attemptLimit" (2) should be greater then the value of "successThreshold" (2).'); + await assertRaisesError('--quarantine-mode attemptLimit=2,successThreshold=2', 'The value of "attemptLimit" (2) should be greater then the value of "successThreshold" (2).'); + + await assertRaisesError('-q attemptLimit=2,successThreshold=3', 'The value of "attemptLimit" (2) should be greater then the value of "successThreshold" (3).'); + await assertRaisesError('--quarantine-mode attemptLimit=2,successThreshold=3', 'The value of "attemptLimit" (2) should be greater then the value of "successThreshold" (3).'); + }); + + it('Should fail if "attemptLimit" is less than 3 with the default "successThreshold" value (3)', async () => { + await assertRaisesError('-q attemptLimit=2', 'The value of "attemptLimit" (2) should be greater then the value of "successThreshold" (3).'); + await assertRaisesError('--quarantine-mode attemptLimit=2', 'The value of "attemptLimit" (2) should be greater then the value of "successThreshold" (3).'); + }); + + it('Should fail if "attemptLimit" is less than 2', async () => { + await assertRaisesError('-q attemptLimit=1', 'The "attemptLimit" parameter only accepts values of 2 and up.'); + await assertRaisesError('--quarantine-mode attemptLimit=1', 'The "attemptLimit" parameter only accepts values of 2 and up.'); + + await assertRaisesError('-q attemptLimit=0', 'The "attemptLimit" parameter only accepts values of 2 and up.'); + await assertRaisesError('--quarantine-mode attemptLimit=0', 'The "attemptLimit" parameter only accepts values of 2 and up.'); + }); + + it('Should fail if "successThreshold" is less than 1', async () => { + await assertRaisesError('-q successThreshold=0', 'The "successThreshold" parameter only accepts values of 1 and up.'); + await assertRaisesError('--quarantine-mode successThreshold=0', 'The "successThreshold" parameter only accepts values of 1 and up.'); + }); + + it('Should not fail if the quarantine option is not the latest option and no quarantine mode arguments are specified', async () => { + async function checkCliArgs (argsString) { + const parser = await parse(argsString); + + expect(parser.opts.quarantineMode).to.be.ok; + expect(parser.opts.browsers).eql(['chrome']); + expect(parser.opts.src).eql(['test.js']); + } + + await checkCliArgs('-q chrome test.js'); + await checkCliArgs('--quarantine-mode chrome test.js'); + await checkCliArgs('chrome -q test.js'); + await checkCliArgs('chrome --quarantine-mode test.js'); + }); + }); + it('Should parse command line arguments', function () { - return parse('-r list -S -q -e --hostname myhost --proxy localhost:1234 --qr-code --app run-app --speed 0.5 ie test/server/data/file-list/file-1.js') - .then(function (parser) { - expect(parser.browsers).eql(['ie']); - expect(parser.src).eql([path.resolve(process.cwd(), 'test/server/data/file-list/file-1.js')]); - expect(parser.opts.reporter).eql('list'); + return parse('-r list -S -q -e message=/testMessage/i,stack=testStack,pageUrl=testPageUrl --hostname myhost --base-url localhost:3000 --proxy localhost:1234 --proxy-bypass localhost:5678 --qr-code --app run-app --speed 0.5 --debug-on-fail --disable-page-reloads --retry-test-pages --dev --sf --disable-page-caching --disable-http2 --disable-native-automation --disable-cross-domain edge test/server/data/file-list/file-1.js') + .then(parser => { + expect(parser.opts.browsers).eql(['edge']); + expect(parser.opts.src).eql(['test/server/data/file-list/file-1.js']); + expect(parser.opts.reporter[0].name).eql('list'); expect(parser.opts.hostname).eql('myhost'); expect(parser.opts.app).eql('run-app'); - expect(parser.opts.screenshots).to.be.undefined; - expect(parser.opts.screenshotsOnFails).to.be.ok; + expect(parser.opts.screenshots.takeOnFails).to.be.ok; + expect(parser.opts.screenshots.path).to.be.undefined; + expect(parser.opts.screenshots.fullPage).to.be.undefined; + expect(parser.opts.screenshots.pathPattern).to.be.undefined; expect(parser.opts.quarantineMode).to.be.ok; - expect(parser.opts.skipJsErrors).to.be.ok; + expect(parser.opts.skipJsErrors.message).eql('/testMessage/i'); + expect(parser.opts.skipJsErrors.stack).eql('testStack'); + expect(parser.opts.skipJsErrors.pageUrl).eql('testPageUrl'); + expect(parser.opts.dev).to.be.ok; expect(parser.opts.speed).eql(0.5); expect(parser.opts.qrCode).to.be.ok; expect(parser.opts.proxy).to.be.ok; + expect(parser.opts.proxyBypass).to.be.ok; + expect(parser.opts.debugOnFail).to.be.ok; + expect(parser.opts.stopOnFirstFail).to.be.ok; + expect(parser.opts.disablePageCaching).to.be.ok; + expect(parser.opts.disablePageReloads).to.be.ok; + expect(parser.opts.retryTestPages).to.be.ok; + expect(parser.opts.disableHttp2).to.be.ok; + expect(parser.opts.disableCrossDomain).to.be.ok; + expect(parser.opts.disableNativeAutomation).to.be.ok; + expect(parser.opts.baseUrl).eql('localhost:3000'); }); }); -}); + it('Should parse unknown command line arguments', function () { + const unknownArgument = '--unknown-argument=unknown-value'; + + return parse(unknownArgument) + .then(parser => { + expect(parser.args).eql([unknownArgument]); + }); + }); + + it('Should have static CLI for the main command', () => { + const CHANGE_CLI_WARNING = 'IMPORTANT: Please be sure what you want to change CLI if this test is failing!'; + const ADD_TO_RUN_OPTIONS_WARNING = 'Check that the added option is correctly passed from the command-line interface to the run options.' + + 'If the new option is not a run option just increase the "expectedOtherOptionsCount" value'; + + const EXPECTED_OPTIONS = [ + { long: '--version', short: '-v' }, + { long: '--list-browsers', short: '-b' }, + { long: '--reporter', short: '-r' }, + { long: '--screenshots', short: '-s' }, + { long: '--screenshot-path-pattern', short: '-p' }, + { long: '--screenshots-on-fails', short: '-S' }, + { long: '--quarantine-mode', short: '-q' }, + { long: '--debug-mode', short: '-d' }, + { long: '--skip-js-errors', short: '-e' }, + { long: '--test', short: '-t' }, + { long: '--test-grep', short: '-T' }, + { long: '--fixture', short: '-f' }, + { long: '--fixture-grep', short: '-F' }, + { long: '--app', short: '-a' }, + { long: '--concurrency', short: '-c' }, + { long: '--live', short: '-L' }, + { long: '--test-meta' }, + { long: '--fixture-meta' }, + { long: '--debug-on-fail' }, + { long: '--app-init-delay' }, + { long: '--selector-timeout' }, + { long: '--assertion-timeout' }, + { long: '--page-load-timeout' }, + { long: '--browser-init-timeout' }, + { long: '--test-execution-timeout' }, + { long: '--run-execution-timeout' }, + { long: '--speed' }, + { long: '--ports' }, + { long: '--hostname' }, + { long: '--proxy' }, + { long: '--proxy-bypass' }, + { long: '--dev' }, + { long: '--ssl' }, + { long: '--qr-code' }, + { long: '--skip-uncaught-errors', short: '-u' }, + { long: '--color' }, + { long: '--no-color' }, + { long: '--stop-on-first-fail', short: '--sf' }, + { long: '--video' }, + { long: '--video-options' }, + { long: '--video-encoding-options' }, + { long: '--config-file' }, + { long: '--ts-config-path' }, + { long: '--client-scripts', short: '--cs' }, + { long: '--disable-page-caching' }, + { long: '--disable-page-reloads' }, + { long: '--retry-test-pages' }, + { long: '--disable-screenshots' }, + { long: '--screenshots-full-page' }, + { long: '--disable-multiple-windows' }, + { long: '--compiler-options' }, + { long: '--page-request-timeout' }, + { long: '--ajax-request-timeout' }, + { long: '--cache' }, + { long: '--disable-http2' }, + { long: '--disable-native-automation' }, + { long: '--base-url' }, + { long: '--disable-cross-domain' }, + { long: '--esm' }, + { long: '--experimental-multiple-windows' }, + ]; + + const parser = new CliArgumentParser(''); + const options = parser.testCafeCommand.options; + + expect(options.length).eql(EXPECTED_OPTIONS.length, CHANGE_CLI_WARNING); + + for (let i = 0; i < EXPECTED_OPTIONS.length; i++) { + const option = find(options, EXPECTED_OPTIONS[i]); + + expect(option).not.eql(void 0, CHANGE_CLI_WARNING); + expect(option.long).eql(EXPECTED_OPTIONS[i].long, CHANGE_CLI_WARNING); + expect(option.short).eql(EXPECTED_OPTIONS[i].short, CHANGE_CLI_WARNING); + } + + const expectedRunOptionsCount = 23; + const expectedOtherOptionsCount = 37; + const otherOptionsCount = options.length - expectedRunOptionsCount; + + expect(runOptionNames.length).eql(expectedRunOptionsCount, ADD_TO_RUN_OPTIONS_WARNING); + expect(otherOptionsCount).eql(expectedOtherOptionsCount, ADD_TO_RUN_OPTIONS_WARNING); + }); + + it('Run options', () => { + const argumentsString = 'edge,chrome test.js' + [ + '--debug-on-fail', + '--skip-js-errors', + '--skip-uncaught-errors', + '--quarantine-mode', + '--debug-mode', + '--debug-on-fail', + '--selector-timeout 1000', + '--assertion-timeout 1000', + '--page-load-timeout 1000', + '--browser-init-timeout 1000', + '--test-execution-timeout 1000', + '--run-execution-timeout 1000', + '--speed 1', + '--stop-on-first-fail', + '--disable-page-caching', + '--disable-page-reloads', + '--disable-screenshots', + '--disable-multiple-windows', + '--base-url localhost:3000', + ].join(' '); + + return parse(argumentsString) + .then(parser => { + const runOpts = parser.getRunOptions(); + + expect(runOpts.skipJsErrors).eql(true); + expect(runOpts.skipUncaughtErrors).eql(true); + expect(runOpts.quarantineMode).eql(true); + expect(runOpts.debugMode).eql(true); + expect(runOpts.debugOnFail).eql(true); + expect(runOpts.selectorTimeout).eql(1000); + expect(runOpts.assertionTimeout).eql(1000); + expect(runOpts.pageLoadTimeout).eql(1000); + expect(runOpts.browserInitTimeout).eql(1000); + expect(runOpts.testExecutionTimeout).eql(1000); + expect(runOpts.runExecutionTimeout).eql(1000); + expect(runOpts.speed).eql(1); + expect(runOpts.stopOnFirstFail).eql(true); + expect(runOpts.disablePageCaching).eql(true); + expect(runOpts.disablePageReloads).eql(true); + expect(runOpts.disableScreenshots).eql(true); + expect(runOpts.disableMultipleWindows).eql(true); + expect(runOpts.browsers).to.be.undefined; + expect(runOpts.baseUrl).eql('localhost:3000'); + }); + }); +}); diff --git a/test/server/cli-authentication-helper-test.js b/test/server/cli-authentication-helper-test.js new file mode 100644 index 00000000000..ba1a2aade48 --- /dev/null +++ b/test/server/cli-authentication-helper-test.js @@ -0,0 +1,178 @@ +const { noop } = require('lodash'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const sinon = require('sinon'); +const Timer = require('../../lib/utils/timer'); + + +describe('CLI', () => { + describe('Authentication helper', () => { + it('Should retry until permissions granted', async () => { + const getAnyKey = sinon.stub(); + const TimerConstructor = sinon.stub(); + const log = { write: sinon.stub() }; + + const authFunction = sinon.stub(); + + TimerConstructor.returns({ expired: false, promise: new Promise(noop) }); + + getAnyKey + .onCall(0).resolves() + .onCall(1).resolves(); + + authFunction + .onCall(0).rejects(Error('Call 0')) + .onCall(1).rejects(Error('Call 1')) + .onCall(2).resolves('OK'); + + const authenticationHelper = proxyquire('../../lib/cli/authentication-helper', { + './log': log, + '../utils/timer': TimerConstructor, + '../utils/get-any-key': getAnyKey, + }); + + const { result, error } = await authenticationHelper(authFunction, Error); + + expect(error).be.undefined; + expect(result).equal('OK'); + expect(getAnyKey.callCount).equal(2); + expect(log.write.callCount).equal(2); + + expect( + log.write.alwaysCalledWith( + 'TestCafe requires permission to record the screen. ' + + 'Open \'System Preferences > Security & Privacy > Privacy > Screen Recording\' and check ' + + '\'TestCafe Browser Tools\' in the application list.\n' + + '\n' + + 'Press any key to retry.' + ), + log.write.args + ).be.true; + }); + + it('Should retry until the timeout expires until permissions granted', async () => { + const getAnyKey = sinon.stub(); + const TimerConstructor = sinon.stub(); + const log = { write: sinon.stub() }; + + const authFunction = sinon.stub(); + + const fakeTimer = new Timer(0); + + TimerConstructor.returns(fakeTimer); + + getAnyKey.resolves(new Promise(noop)); + + authFunction + .onCall(0).rejects(Error('Call 0')) + .onCall(1).resolves('OK'); + + const authenticationHelper = proxyquire('../../lib/cli/authentication-helper', { + './log': log, + '../utils/timer': TimerConstructor, + '../utils/get-any-key': getAnyKey, + }); + + const { result, error } = await authenticationHelper(authFunction, Error); + + expect(error).be.undefined; + expect(result).equal('OK'); + expect(TimerConstructor.callCount).equal(1); + expect(TimerConstructor.args[0]).deep.equal([30000]); + expect(log.write.callCount).equal(1); + + expect(log.write.args[0]).deep.equal([ + 'TestCafe requires permission to record the screen. ' + + 'Open \'System Preferences > Security & Privacy > Privacy > Screen Recording\' and check ' + + '\'TestCafe Browser Tools\' in the application list.\n' + + '\n' + + 'Press any key to retry.', + ]); + }); + + it('Should return an error if the timeout expires and no permissions granted', async () => { + const getAnyKey = sinon.stub(); + const TimerConstructor = sinon.stub(); + const log = { write: sinon.stub() }; + + const authFunction = sinon.stub(); + + const fakeTimer = new Timer(0); + + TimerConstructor.returns(fakeTimer); + + getAnyKey.resolves(new Promise(noop)); + + authFunction.rejects(Error('Call')); + + const authenticationHelper = proxyquire('../../lib/cli/authentication-helper', { + './log': log, + '../utils/timer': TimerConstructor, + '../utils/get-any-key': getAnyKey, + }); + + const { result, error } = await authenticationHelper(authFunction, Error); + + expect(error.message).equal('Call'); + expect(result).be.undefined; + expect(authFunction.callCount).equal(2); + expect(TimerConstructor.callCount).equal(1); + expect(TimerConstructor.args[0]).deep.equal([30000]); + expect(getAnyKey.callCount).equal(1); + expect(log.write.callCount).equal(1); + + expect(log.write.args[0]).deep.equal([ + 'TestCafe requires permission to record the screen. ' + + 'Open \'System Preferences > Security & Privacy > Privacy > Screen Recording\' and check ' + + '\'TestCafe Browser Tools\' in the application list.\n' + + '\n' + + 'Press any key to retry.', + ]); + }); + + it('Should throw if an unexpected error occurs', async () => { + class CustomError extends Error { + + } + + const getAnyKey = sinon.stub(); + const TimerConstructor = sinon.stub(); + const log = { write: sinon.stub() }; + + const authFunction = sinon.stub(); + + const fakeTimer = new Timer(0); + + TimerConstructor.returns(fakeTimer); + + getAnyKey.resolves(new Promise(noop)); + + authFunction.rejects(Error('Unexpected')); + + const authenticationHelper = proxyquire('../../lib/cli/authentication-helper', { + './log': log, + '../utils/timer': TimerConstructor, + '../utils/get-any-key': getAnyKey, + }); + + let unexpectedError = null; + let result = null; + let error = null; + + try { + ({ result, error } = await authenticationHelper(authFunction, CustomError)); + } + catch (e) { + unexpectedError = e; + } + + expect(unexpectedError.message).equal('Unexpected'); + expect(result).be.null; + expect(error).be.null; + expect(authFunction.callCount).equal(1); + expect(TimerConstructor.callCount).equal(0); + expect(getAnyKey.callCount).equal(0); + expect(log.write.callCount).equal(0); + }); + }); +}); diff --git a/test/server/cli-correct-browsers-and-sources-test.js b/test/server/cli-correct-browsers-and-sources-test.js new file mode 100644 index 00000000000..27ba8665847 --- /dev/null +++ b/test/server/cli-correct-browsers-and-sources-test.js @@ -0,0 +1,147 @@ +const expect = require('chai').expect; +const sinon = require('sinon'); +const correctBrowsersAndSources = require('../../lib/cli/correct-browsers-and-sources'); +const browserProviderPool = require('../../lib/browser/provider/pool'); + + +class ConfigurationMock { + constructor (opts) { + this.mergeOptions(opts); + } + + mergeOptions (opts) { + Object.keys(opts).forEach(key => { + this[key] = opts[key]; + }); + } + + getOption (name) { + return this[name]; + } +} + +class ArgsMock { + constructor (args) { + this.args = args; + this.opts = { + browsers: args[0] && args[0].split(','), + src: args.slice(1), + }; + } +} +describe('CLI Correct browsers and sources', () => { + before(() => { + sinon.stub(browserProviderPool, 'getBrowserInfo').callsFake(browserName => { + if (!browserName.startsWith('browser')) + return Promise.reject(new Error(`Not found ${browserName}`)); + + return Promise.resolve({}); + }); + }); + + after(() => { + browserProviderPool.getBrowserInfo.restore(); + }); + + it('Should allow to skip browser in CLI if they are specified in the config', () => { + const configuration = new ConfigurationMock({ + browsers: ['browser'], + }); + + const args = new ArgsMock([ + 'test1.js', + 'test2.js', + ]); + + return correctBrowsersAndSources(args, configuration) + .then(({ browsers, sources }) => { + expect(browsers).to.be.empty; + expect(sources).to.be.deep.equal(['test1.js', 'test2.js']); + }); + }); + + it('Should override browsers in the config with valid browsers from CLI', () => { + const configuration = new ConfigurationMock({ + browsers: ['browser1'], + }); + + const args = new ArgsMock([ + 'browser2,browser3', + 'test1.js', + ]); + + return correctBrowsersAndSources(args, configuration) + .then(({ browsers, sources }) => { + expect(browsers).to.be.deep.equal(['browser2', 'browser3']); + expect(sources).to.be.deep.equal(['test1.js']); + }); + }); + + it('Should not correct browsers and sources from CLI if browsers are not specified in the config', () => { + const configuration = new ConfigurationMock({ + browsers: [], + }); + + const args = new ArgsMock([ + 'foo-test.js', + 'bar-test.js', + ]); + + return correctBrowsersAndSources(args, configuration) + .then(({ browsers, sources }) => { + expect(browsers).to.be.deep.equal(['foo-test.js']); + expect(sources).to.be.deep.equal(['bar-test.js']); + }); + }); + + it('Should not override test files from the config with a valid CLI contains commas', () => { + const configuration = new ConfigurationMock({ + browsers: ['browser'], + }); + + const args = new ArgsMock([ + '[e2e,admin]user-manager-test.js', + '[e2e,store]basket-page-test.js', + ]); + + return correctBrowsersAndSources(args, configuration) + .then(({ browsers, sources }) => { + expect(browsers).to.be.deep.equal([]); + expect(sources).to.be.deep.equal(['[e2e,admin]user-manager-test.js', '[e2e,store]basket-page-test.js']); + }); + }); + + it('Should handle empty CLI arguments', () => { + const configuration = new ConfigurationMock({ + browsers: ['browser'], + }); + + const args = new ArgsMock([]); + + return correctBrowsersAndSources(args, configuration) + .then(({ browsers, sources }) => { + expect(browsers).to.be.empty; + expect(sources).to.be.empty; + }); + }); + + it('Should throw an error if browsers from the config are overridden with valid & invalid browsers from CLI', () => { + const configuration = new ConfigurationMock({ + browsers: ['browser1'], + }); + + const args = new ArgsMock([ + 'browser2,foo,bar', + 'test1.js', + ]); + + return correctBrowsersAndSources(args, configuration) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(error => { + expect(error.message).to.contain('foo'); + expect(error.message).to.contain('bar'); + }); + }); +}); diff --git a/test/server/cli-remote-wizard-test.js b/test/server/cli-remote-wizard-test.js new file mode 100644 index 00000000000..ff87bdba48a --- /dev/null +++ b/test/server/cli-remote-wizard-test.js @@ -0,0 +1,54 @@ +const EventEmitter = require('events'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const sinon = require('sinon'); +const { constructor: Chalk } = require('chalk'); +const { noop } = require('lodash'); +const { configurationMock } = require('./helpers/mocks'); + + +describe('[CLI] Remote wizard', () => { + it('Should log the connection url', async () => { + const log = { + write: sinon.stub(), + hideSpinner: sinon.stub(), + showSpinner: sinon.stub(), + }; + + const testCafe = { + browserConnectionGateway: { + connectUrl: 'http://example.com', + }, + + initializeBrowserConnectionGateway: noop, + + configuration: configurationMock, + + createBrowserConnection: () => { + const connection = new EventEmitter(); + + connection.userAgent = 'USER-AGENT'; + + setTimeout(() => connection.emit('ready'), 200); + + return Promise.resolve(connection); + }, + }; + + const remoteWizard = proxyquire('../../lib/cli/remotes-wizard', { + './log': log, + 'chalk': new Chalk({ level: 0 }), + }); + + await remoteWizard(testCafe, 1, false); + + const output = log.write.args.map(call => call[0]).join('\n'); + + expect(output).equal([ + 'Connecting 1 remote browser(s)...', + 'Navigate to the following URL from each remote browser.', + 'Connect URL: http://example.com', + 'CONNECTED USER-AGENT', + ].join('\n')); + }); +}); diff --git a/test/server/compiler-test.js b/test/server/compiler-test.js index cd8810db371..9bf2cb82ea1 100644 --- a/test/server/compiler-test.js +++ b/test/server/compiler-test.js @@ -1,30 +1,61 @@ -var expect = require('chai').expect; -var resolve = require('path').resolve; -var readFile = require('fs').readFileSync; -var Promise = require('pinkie'); -var renderers = require('callsite-record').renderers; -var ERR_TYPE = require('../../lib/errors/test-run/type'); -var exportableLib = require('../../lib/api/exportable-lib'); -var NODE_VER = require('../../lib/utils/node-version'); -var createStackFilter = require('../../lib/errors/create-stack-filter.js'); -var assertError = require('./helpers/assert-error').assertError; -var compile = require('./helpers/compile'); +const { exec } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { promisify } = require('util'); +const { expect } = require('chai'); +const proxyquire = require('proxyquire'); +const sinon = require('sinon'); +const globby = require('globby'); +const { nanoid } = require('nanoid'); +const dedent = require('dedent'); +const { TEST_RUN_ERRORS } = require('../../lib/errors/types'); +const exportableLib = require('../../lib/api/exportable-lib'); +const createStackFilter = require('../../lib/errors/create-stack-filter.js'); +const TestController = require('../../lib/api/test-controller'); +const { assertError } = require('./helpers/assert-runtime-error'); +const compile = require('./helpers/compile'); +const Module = require('module'); +const toPosixPath = require('../../lib/utils/to-posix-path'); +const BaseTestRunMock = require('./helpers/base-test-run-mock'); +const getTestCafeVersion = require('../../lib/utils/get-testcafe-version'); + +const copy = promisify(fs.copyFile); +const remove = promisify(fs.unlink); +const writeFile = promisify(fs.writeFile); + +require('source-map-support').install(); + +class TestRunMock extends BaseTestRunMock { + _internalExecuteCommand (command) { + this.commands.push(command); + + return this.expectedError ? Promise.reject(new Error(this.expectedError)) : Promise.resolve(); + } + + constructor (expectedError) { + super(); + + this.expectedError = expectedError; + this.commands = []; + } +} describe('Compiler', function () { - var testRunMock = { id: 'yo' }; + const testRunMock = new TestRunMock(); this.timeout(20000); - // FIXME: Babel errors always contain POSIX-format file paths. - function posixResolve (path) { - return resolve(path).replace(new RegExp('\\\\', 'g'), '/'); + // NOTE: TypeScript compiler resolves paths in the POSIX-format. + function posixResolve (pathname) { + return toPosixPath(path.resolve(pathname)); } it('Should compile mixed content', function () { - var sources = [ + const sources = [ 'test/server/data/test-suites/mixed-content/testfile.js', 'test/server/data/test-suites/mixed-content/legacy.test.js', - 'test/server/data/test-suites/mixed-content/non-testfile.js' + 'test/server/data/test-suites/mixed-content/non-testfile.js', ]; return compile(sources) @@ -32,6 +63,7 @@ describe('Compiler', function () { expect(compiled.tests.length).eql(2); expect(compiled.tests[0].name).eql('1.Test'); + expect(compiled.tests[0].meta).eql({ run: 'run-001' }); expect(compiled.tests[0].isLegacy).to.be.undefined; expect(compiled.tests[1].name).eql('2.LegacyTest'); @@ -40,21 +72,42 @@ describe('Compiler', function () { }); describe('ES-next', function () { + it('Should compile test defined in separate module if option is enabled', function () { + const sources = [ + 'test/server/data/test-suites/test-as-module/with-tests/testfile.js', + ]; + + return compile(sources, true) + .then(function (compiled) { + const tests = compiled.tests; + const fixtures = compiled.fixtures; + + expect(tests.length).eql(1); + expect(fixtures.length).eql(1); + + expect(tests[0].name).eql('test'); + expect(fixtures[0].name).eql('Library tests'); + }); + }); + it('Should compile test files and their dependencies', function () { - var sources = [ + const sources = [ 'test/server/data/test-suites/basic/testfile1.js', - 'test/server/data/test-suites/basic/testfile2.js' + 'test/server/data/test-suites/basic/testfile2.js', + 'test/server/data/test-suites/basic/testfile4.js', ]; + const testcafeVersion = getTestCafeVersion(); + return compile(sources) .then(function (compiled) { - var testfile1 = resolve('test/server/data/test-suites/basic/testfile1.js'); - var testfile2 = resolve('test/server/data/test-suites/basic/testfile2.js'); - var tests = compiled.tests; - var fixtures = compiled.fixtures; + const testfile1 = path.resolve('test/server/data/test-suites/basic/testfile1.js'); + const testfile2 = path.resolve('test/server/data/test-suites/basic/testfile2.js'); + const tests = compiled.tests; + const fixtures = compiled.fixtures; - expect(tests.length).eql(4); - expect(fixtures.length).eql(3); + expect(tests.length).eql(5); + expect(fixtures.length).eql(4); expect(fixtures[0].name).eql('Fixture1'); expect(fixtures[0].path).eql(testfile1); @@ -89,11 +142,64 @@ describe('Compiler', function () { 'F1T1: Hey from dep1', 'F1T2', 'F2T1', - 'F3T1: Hey from dep1 and dep2' + 'F3T1: Hey from dep1 and dep2', + testcafeVersion, ]); + expect(results[results.length - 1]).to.be.a('string'); }); }); + it('Should recompile required files', async function () { + const sources = [ + 'test/server/data/test-suites/separate-cache-for-required-modules/testfile1.js', + ]; + + const compileFn = () => compile(sources) + .then(function (compiled) { + const tests = compiled.tests; + + return Promise.all(tests.map(function (test) { + return test.fn(testRunMock); + })); + }) + .then(function (results) { + return results; + }); + + const res1 = (await compileFn())[0]; + const res2 = (await compileFn())[0]; + + expect(res1.noncached).not.eql(res2.noncached); + expect(res1.cached).eql(res2.cached); + }); + + it('Should compile basic JSX', async function () { + const sources = [ + 'test/server/data/test-suites/compile-react/testfile.jsx', + ]; + + const compiled = await compile(sources); + + const testfile = path.resolve('test/server/data/test-suites/compile-react/testfile.jsx'); + const tests = compiled.tests; + const fixtures = compiled.fixtures; + + expect(tests.length).eql(1); + expect(fixtures.length).eql(1); + + expect(fixtures[0].name).eql('JSX'); + expect(fixtures[0].path).eql(testfile); + expect(fixtures[0].pageUrl).eql('about:blank'); + + expect(tests[0].name).eql('Test React'); + expect(tests[0].fixture).eql(fixtures[0]); + + const results = await tests[0].fn(testRunMock); + + expect(results.type).eql('h2'); + expect(results.props.children).eql('Hello React'); + }); + it('Should provide exportable lib dep', function () { return compile('test/server/data/test-suites/exportable-lib-dep/testfile.js') .then(function (compiled) { @@ -115,12 +221,13 @@ describe('Compiler', function () { }); }); + // NOTE: https://github.com/babel/babel/issues/12261 it('Should strip Flow type declarations if a marker comment presents', function () { return compile('test/server/data/test-suites/flow-type-declarations/testfile.js') - .then(function (compiled) { + .then(compiled => { return compiled.tests[0].fn(testRunMock); }) - .then(function (results) { + .then(results => { expect(results.repeated1).to.equal('yoyoyoyoyoyoyoyoyoyoyoyoyo'); expect(results.repeated2).to.equal('yoyoyoyoyoyoyoyoyoyoyoyoyo'); expect(results.length).to.equal(5); @@ -129,22 +236,57 @@ describe('Compiler', function () { expect(results.inventory).to.equal('42 yoyo'); }); }); - }); + it('Should compile test with static class blocks', function () { + const sources = [ + 'test/server/data/test-suites/class-with-static/testfile.js', + ]; + + return compile(sources) + .then(function (compiled) { + const tests = compiled.tests; + const fixtures = compiled.fixtures; + + expect(tests.length).eql(1); + expect(fixtures.length).eql(1); + + expect(tests[0].name).eql('Test'); + expect(fixtures[0].name).eql('Fixture'); + }); + }); + }); describe('TypeScript', function () { + it('Should compile test defined in separate module if option is enabled', function () { + const sources = [ + 'test/server/data/test-suites/test-as-module/with-tests/testfile.ts', + ]; + + return compile(sources, true) + .then(function (compiled) { + const tests = compiled.tests; + const fixtures = compiled.fixtures; + + expect(tests.length).eql(1); + expect(fixtures.length).eql(1); + + expect(tests[0].name).eql('test'); + expect(fixtures[0].name).eql('Library tests'); + }); + }); + it('Should compile test files and their dependencies', function () { - var sources = [ + const sources = [ 'test/server/data/test-suites/typescript-basic/testfile1.ts', - 'test/server/data/test-suites/typescript-basic/testfile2.ts' + 'test/server/data/test-suites/typescript-basic/testfile2.ts', ]; return compile(sources) .then(function (compiled) { - var testfile1 = resolve('test/server/data/test-suites/typescript-basic/testfile1.ts'); - var testfile2 = resolve('test/server/data/test-suites/typescript-basic/testfile2.ts'); - var tests = compiled.tests; - var fixtures = compiled.fixtures; + const testfile1 = path.resolve('test/server/data/test-suites/typescript-basic/testfile1.ts'); + const testfile2 = path.resolve('test/server/data/test-suites/typescript-basic/testfile2.ts'); + const tests = compiled.tests; + const fixtures = compiled.fixtures; expect(tests.length).eql(4); expect(fixtures.length).eql(3); @@ -182,11 +324,38 @@ describe('Compiler', function () { 'F1T1: Hey from dep1', 'F1T2', 'F2T1', - 'F3T1: Hey from dep1 and dep2' + 'F3T1: Hey from dep1 and dep2', ]); }); }); + it('Should compile basic TSX', async function () { + const sources = [ + 'test/server/data/test-suites/compile-react/testfile.tsx', + ]; + + const compiled = await compile(sources); + + const testfile = path.resolve('test/server/data/test-suites/compile-react/testfile.tsx'); + const tests = compiled.tests; + const fixtures = compiled.fixtures; + + expect(tests.length).eql(1); + expect(fixtures.length).eql(1); + + expect(fixtures[0].name).eql('TSX'); + expect(fixtures[0].path).eql(testfile); + expect(fixtures[0].pageUrl).eql('about:blank'); + + expect(tests[0].name).eql('Test React'); + expect(tests[0].fixture).eql(fixtures[0]); + + const results = await tests[0].fn(testRunMock); + + expect(results.type).eql('h2'); + expect(results.props.children).eql('Hello React'); + }); + it('Should compile mixed dependencies', function () { return compile('test/server/data/test-suites/typescript-mixed-dep/testfile.ts') .then(function (compiled) { @@ -197,14 +366,71 @@ describe('Compiler', function () { }); }); + it('Should compile ts-definitions successfully with the `--strict` option enabled', function () { + this.timeout(60000); + + const tscPath = path.resolve('node_modules/.bin/tsc'); + const defsPath = path.resolve('ts-defs/index.d.ts'); + const args = '--strict'; + const command = `${tscPath} ${defsPath} ${args} --target ES6 --noEmit --moduleResolution node`; + + return new Promise(resolve => { + exec(command, (error, stdout) => { + resolve({ error, stdout }); + }); + }).then(value => { + expect(value.stdout).eql(''); + expect(value.error).is.null; + }); + }); + + it('Should have definitions for all TestController methods', async function () { + this.timeout(60000); + + const apiMethods = TestController.API_LIST + .filter(prop => !prop.accessor) + .map(prop => prop.apiProp); + + const possibleErrors = apiMethods.map(method => `Property '${method}' does not exist on type 'TestController'`); + const actualErrors = []; + + const testCode = apiMethods + .map(prop => dedent` + fixture('${prop}').page('http://example.com'); + + test('${prop}', async t => { + await t.${prop}(); + }); + `).join(''); + + const tempTestFilePath = path.join(process.cwd(), `tmp-ts-definitions-test.ts`); + + await writeFile(tempTestFilePath, testCode); + + try { + await compile(tempTestFilePath); + } + catch (err) { + for (const errMsg of possibleErrors) { + if (err.data[0].includes(errMsg)) + actualErrors.push(errMsg); + } + } + + await remove(tempTestFilePath); + + expect(actualErrors.join('\n')).eql(''); + }); + it('Should provide API definitions', function () { - var src = [ - 'test/server/data/test-suites/typescript-defs/structure.ts', - 'test/server/data/test-suites/typescript-defs/selectors.ts', - 'test/server/data/test-suites/typescript-defs/client-functions.ts', - 'test/server/data/test-suites/typescript-defs/roles.ts', - 'test/server/data/test-suites/typescript-defs/test-controller.ts' - ]; + this.timeout(60000); + + const typescriptDefsFolder = 'test/server/data/test-suites/typescript-defs/'; + const src = []; + + fs.readdirSync(typescriptDefsFolder).forEach(file => { + src.push(path.join(typescriptDefsFolder, file)); + }); return compile(src).then(function (compiled) { expect(compiled.tests.length).gt(0); @@ -222,18 +448,287 @@ describe('Compiler', function () { }); }); + it('Should import pure TypeScript dependency module', () => { + return compile('test/server/data/test-suites/typescript-pure-ts-module-dep/testfile.ts') + .then(function (compiled) { + return compiled.tests[0].fn(testRunMock); + }) + .then(function (result) { + expect(result.exportableLib).eql(exportableLib); + expect(result.exportableLib).eql(result.exportableLibInDep); + }); + }); + + it('Should start and terminate runner w/out errors', () => { + return compile('test/server/data/test-suites/typescript-runner/runner.ts') + .then(function (compiled) { + expect(compiled.tests.length).gt(0); + }); + }); + + it('Should not recompile cached files', async () => { + const ts = require('typescript'); + const createProgram = sinon.stub().callsFake(ts.createProgram); + + const TSCompiler = proxyquire('../../lib/compiler/test-file/formats/typescript/compiler', { + 'typescript': { createProgram }, + }); + + const testData = [{ filename: 'test/server/data/test-suites/typescript-basic/testfile1.ts', code: 'console.log(42)' }]; + const tsCompiler = new TSCompiler(); + + await tsCompiler.precompile(testData); + await tsCompiler.precompile(testData); + + expect(createProgram.callCount).eql(1); + }); + + it('Should provide correct globals in TestCafe scripts', async function () { + this.timeout(60000); + + const tscPath = path.resolve('node_modules/.bin/tsc'); + const defsPath = path.resolve('ts-defs/testcafe-scripts.d.ts'); + const scriptPaths = await globby('test/server/data/test-suites/typescript-testcafe-scripts-defs/*.ts'); + const command = `${tscPath} ${defsPath} ${scriptPaths.join(' ')} --target ES6 --noEmit --moduleResolution node`; + + return new Promise(resolve => { + exec(command, (error, stdout) => { + resolve({ error, stdout }); + }); + }).then(value => { + expect(value.stdout).eql(''); + expect(value.error).is.null; + }); + }); + + it('Should provide correct globals for selector and client-functions only', async function () { + this.timeout(60000); + + const tscPath = path.resolve('node_modules/.bin/tsc'); + const defsPath = path.resolve('ts-defs/selectors.d.ts'); + const scriptPaths = await globby('test/server/data/test-suites/typescript-selectors-defs/*.ts'); + const command = `${tscPath} ${defsPath} ${scriptPaths.join(' ')} --target ES6 --noEmit --moduleResolution node`; + + return new Promise(resolve => { + exec(command, (error, stdout) => { + resolve({ error, stdout }); + }); + }).then(value => { + expect(value.stdout).eql(''); + expect(value.error).is.null; + }); + }); + + it('Should provide correct globals when they are redeclared in node_modules/@types', async () => { + const currentDir = process.cwd(); + const scriptDir = 'test/server/data/test-suites/typescript-test-redeclared-in-types'; + const scriptName = 'testfile.ts'; + + process.chdir(scriptDir); + + try { + return await compile(scriptName); + } + finally { + process.chdir(currentDir); + } + }); + + it('Should provide Node.js global when TestCafe is installed globally', async function () { + this.timeout(60000); + + const tmpFileName = `testfile-${nanoid()}.ts`; + const tmpFileDir = os.tmpdir(); + const tmpFilePath = path.join(tmpFileDir, tmpFileName); + const currentDir = process.cwd(); + + await copy(path.resolve('test/server/data/test-suites/typescript-nodejs-globals/testfile.ts'), tmpFilePath); + + try { + process.chdir(tmpFileDir); + + await compile(tmpFileName); + } + finally { + process.chdir(currentDir); + + await remove(tmpFilePath); + } + }); + + it('Should raise an error on wrong path to the custom compiler module', () => { + const sources = [ + 'test/server/data/test-suites/typescript-basic/testfile1.ts', + ]; + + const options = { + 'typescript': { + 'customCompilerModulePath': 'wrong-path-to-typescript-module', + }, + }; + + return compile(sources, options) + .then(() => { + throw new Error('Promise rejection expected'); + }) + .catch(err => { + assertError(err, { + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Error: Cannot load the TypeScript compiler.\n' + + "Cannot find module 'wrong-path-to-typescript-module'", + }, true); + }); + }); + + describe('Should transform path to the custom compiler module', () => { + function checkPathTransformation (specifiedPath, expectedPath) { + const sources = [ + 'test/server/data/test-suites/typescript-basic/testfile1.ts', + ]; + + const options = { + 'typescript': { }, + }; + + if (specifiedPath !== null) + options.typescript.customCompilerModulePath = specifiedPath; + + let customCompilerModuleIsLoaded = false; + + const storedModuleLoad = Module._load; + + Module._load = function (...args) { + const modulePath = args[0]; + + if (modulePath === expectedPath) { + args[0] = 'typescript'; + + customCompilerModuleIsLoaded = true; + } + + return storedModuleLoad.apply(this, args); + }; + + return compile(sources, options) + .then(() => { + Module._load = storedModuleLoad; + + expect(customCompilerModuleIsLoaded).to.be.true; + }) + .catch(() => { + Module._load = storedModuleLoad; + + expect.fail('compilation should be successful.'); + }); + } + + const repositoryRoot = path.resolve('./'); + + it('Relative', () => { + return checkPathTransformation('../typescript', path.resolve(repositoryRoot, '../typescript')); + }); + + it('Absolute', () => { + const absolutePath = path.resolve(repositoryRoot, './dummy-folder'); + + return checkPathTransformation(absolutePath, absolutePath); + }); + + it('Default', () => { + return checkPathTransformation('typescript', 'typescript'); + }); + + it('Not specified', () => { + return checkPathTransformation(null, 'typescript'); + }); + }); }); + describe('CoffeeScript', function () { + // NOTE: fix this test separately in the context of https://github.com/DevExpress/testcafe/issues/6411 + it.skip('Should compile test defined in separate module if option is enabled', function () { + const sources = [ + 'test/server/data/test-suites/test-as-module/with-tests/testfile.coffee', + ]; + + return compile(sources, true) + .then(function (compiled) { + const tests = compiled.tests; + const fixtures = compiled.fixtures; + + expect(tests.length).eql(1); + expect(fixtures.length).eql(1); + + expect(tests[0].name).eql('test'); + expect(fixtures[0].name).eql('Library tests'); + }); + }); + + it('Should compile test files and their dependencies', function () { + const sources = [ + 'test/server/data/test-suites/coffeescript-basic/testfile1.coffee', + 'test/server/data/test-suites/coffeescript-basic/testfile2.coffee', + ]; + + return compile(sources) + .then(function (compiled) { + const testfile1 = path.resolve('test/server/data/test-suites/coffeescript-basic/testfile1.coffee'); + const testfile2 = path.resolve('test/server/data/test-suites/coffeescript-basic/testfile2.coffee'); + + const tests = compiled.tests; + const fixtures = compiled.fixtures; + + expect(tests.length).eql(4); + expect(fixtures.length).eql(3); + + expect(fixtures[0].name).eql('Fixture1'); + expect(fixtures[0].path).eql(testfile1); + expect(fixtures[0].pageUrl).eql('about:blank'); + + expect(fixtures[1].name).eql('Fixture2'); + expect(fixtures[1].path).eql(testfile1); + expect(fixtures[1].pageUrl).eql('http://example.org'); + + expect(fixtures[2].name).eql('Fixture3'); + expect(fixtures[2].path).eql(testfile2); + expect(fixtures[2].pageUrl).eql('https://example.com'); + + expect(tests[0].name).eql('Fixture1Test1'); + expect(tests[0].fixture).eql(fixtures[0]); + + expect(tests[1].name).eql('Fixture1Test2'); + expect(tests[1].fixture).eql(fixtures[0]); + + expect(tests[2].name).eql('Fixture2Test1'); + expect(tests[2].fixture).eql(fixtures[1]); + + expect(tests[3].name).eql('Fixture3Test1'); + expect(tests[3].fixture).eql(fixtures[2]); + + return Promise.all(tests.map(function (test) { + return test.fn(testRunMock); + })); + }) + .then(function (results) { + expect(results).eql([ + 'F1T1: Hey from dep1', + 'F1T2', + 'F2T1', + 'F3T1: Hey from dep1 and dep2', + ]); + }); + }); + }); describe('RAW file', function () { it('Should compile test files', function () { - var sources = ['test/server/data/test-suites/raw/test.testcafe']; + const sources = ['test/server/data/test-suites/raw/test.testcafe']; return compile(sources) .then(function (compiled) { - var testfile = resolve('test/server/data/test-suites/raw/test.testcafe'); - var tests = compiled.tests; - var fixtures = compiled.fixtures; + const testfile = path.resolve('test/server/data/test-suites/raw/test.testcafe'); + const tests = compiled.tests; + const fixtures = compiled.fixtures; expect(tests.length).eql(3); expect(fixtures.length).eql(2); @@ -258,17 +753,17 @@ describe('Compiler', function () { }); it('Should raise an error if it cannot parse a raw file', function () { - var testfile1 = resolve('test/server/data/test-suites/raw/invalid.testcafe'); - var testfile2 = resolve('test/server/data/test-suites/raw/invalid2.testcafe'); + const testfile1 = path.resolve('test/server/data/test-suites/raw/invalid.testcafe'); + const testfile2 = path.resolve('test/server/data/test-suites/raw/invalid2.testcafe'); return compile(testfile1) .then(function () { throw new Error('Promise rejection is expected'); }) .catch(function (err) { - expect(err.message).contains('Cannot parse a test source file in the raw format at "' + testfile1 + - '" due to an error.\n\n' + - 'SyntaxError: Unexpected token i'); + expect(err.message).contains('Cannot parse a raw test file at "' + testfile1 + + '" due to the following error:\n\n' + + 'SyntaxError:'); }) .then(function () { return compile(testfile2); @@ -277,28 +772,16 @@ describe('Compiler', function () { throw new Error('Promise rejection is expected'); }) .catch(function (err) { - expect(err.message).contains('Cannot parse a test source file in the raw format at "' + testfile2 + - '" due to an error.\n\n'); + expect(err.message).contains('Cannot parse a raw test file at "' + testfile2 + + '" due to the following error:\n\n'); }); }); describe('test.fn()', function () { - var TestRunMock = function (expectedError) { - this.id = 'PPBqWA9'; - this.commands = []; - this.expectedError = expectedError; - }; - - TestRunMock.prototype.executeCommand = function (command) { - this.commands.push(command); - - return this.expectedError ? Promise.reject(new Error(this.expectedError)) : Promise.resolve(); - }; - it('Should be resolved if the test passed', function () { - var sources = ['test/server/data/test-suites/raw/test.testcafe']; - var test = null; - var testRun = new TestRunMock(); + const sources = ['test/server/data/test-suites/raw/test.testcafe']; + let test = null; + const testRun = new TestRunMock(); return compile(sources) .then(function (compiled) { @@ -312,9 +795,9 @@ describe('Compiler', function () { }); it('Should be rejected if the test failed', function () { - var sources = ['test/server/data/test-suites/raw/test.testcafe']; - var expectedError = 'test-error'; - var testRun = new TestRunMock(expectedError); + const sources = ['test/server/data/test-suites/raw/test.testcafe']; + const expectedError = 'test-error'; + const testRun = new TestRunMock(expectedError); return compile(sources) .then(function (compiled) { @@ -324,7 +807,7 @@ describe('Compiler', function () { throw new Error('Promise rejection is expected'); }) .catch(function (errList) { - expect(errList.items[0].type).eql(ERR_TYPE.uncaughtErrorInTestCode); + expect(errList.items[0].code).eql(TEST_RUN_ERRORS.uncaughtErrorInTestCode); expect(errList.items[0].errMsg).contains('test-error'); expect(testRun.commands.length).eql(1); }); @@ -332,7 +815,6 @@ describe('Compiler', function () { }); }); - describe('Client function compilation', function () { function normalizeCode (code) { return code @@ -342,26 +824,17 @@ describe('Compiler', function () { } function getExpected (testDir) { - if (NODE_VER < 4) { - try { - return readFile(testDir + '/expected-node10.js').toString(); - } - catch (err) { - // NOTE: ignore error - we don't have version-specific data - } - } - - return readFile(testDir + '/expected.js').toString(); + return fs.readFileSync(testDir + '/expected.js').toString(); } function testClientFnCompilation (testName) { - var testDir = 'test/server/data/client-fn-compilation/' + testName; - var src = testDir + '/testfile.js'; - var expected = getExpected(testDir); + const testDir = 'test/server/data/client-fn-compilation/' + testName; + const src = testDir + '/testfile.js'; + const expected = getExpected(testDir); return compile(src) .then(function (compiled) { - return compiled.tests[0].fn({ id: 'test' }); + return compiled.tests[0].fn(new TestRunMock()); }) .then(function (compiledClientFn) { expect(normalizeCode(compiledClientFn)).eql(normalizeCode(expected)); @@ -372,20 +845,16 @@ describe('Compiler', function () { return testClientFnCompilation('basic'); }); - it('Should polyfill Babel `Promises` artifacts', function () { - return testClientFnCompilation('promises'); - }); - - it('Should polyfill Babel `Object.keys()` artifacts', function () { - return testClientFnCompilation('object-keys'); - }); + it('Performance (GH-6284)', async function () { + this.timeout(20000); - it('Should polyfill Babel `JSON.stringify()` artifacts', function () { - return testClientFnCompilation('json-stringify'); - }); + const start = new Date().getTime(); + const compiled = await compile('test/server/data/client-fn-compilation/performance/index.js'); + const compilationTime = new Date().getTime() - start; - it('Should polyfill Babel `typeof` artifacts', function () { - return testClientFnCompilation('typeof'); + expect(compilationTime).below(7000); + expect(compiled.tests.length).eql(1); + expect(compiled.fixtures.length).eql(1); }); describe('Regression', function () { @@ -395,7 +864,6 @@ describe('Compiler', function () { }); }); - describe('Errors', function () { it("Should raise an error if the specified source file doesn't exists", function () { return compile('does/not/exists.js') @@ -403,14 +871,18 @@ describe('Compiler', function () { throw new Error('Promise rejection expected'); }) .catch(function (err) { - expect(err.message).eql('Cannot find a test source file at "' + - resolve('does/not/exists.js') + '".'); + expect(err.message).eql('Cannot find a test file at "' + + path.resolve('does/not/exists.js') + '".'); }); }); it('Should raise an error if test dependency has a syntax error', function () { - var testfile = resolve('test/server/data/test-suites/syntax-error-in-dep/testfile.js'); - var dep = posixResolve('test/server/data/test-suites/syntax-error-in-dep/dep.js'); + const testfile = path.resolve('test/server/data/test-suites/syntax-error-in-dep/testfile.js'); + const dep = path.resolve('test/server/data/test-suites/syntax-error-in-dep/dep.js'); + + const stack = [ + testfile, + ]; return compile(testfile) .then(function () { @@ -418,17 +890,22 @@ describe('Compiler', function () { }) .catch(function (err) { assertError(err, { - stackTop: testfile, + stackTop: stack, - message: 'Cannot prepare tests due to an error.\n\n' + - 'SyntaxError: ' + dep + ': Unexpected token, expected { (1:7)' - }); + message: 'Cannot prepare tests due to the following error:\n\n' + + 'SyntaxError: ' + dep + ": Unexpected keyword 'export'. (1:7)", + }, true); }); }); it("Should raise an error if dependency can't require a module", function () { - var testfile = resolve('test/server/data/test-suites/require-error-in-dep/testfile.js'); - var dep = resolve('test/server/data/test-suites/require-error-in-dep/dep.js'); + const testfile = path.resolve('test/server/data/test-suites/require-error-in-dep/testfile.js'); + const dep = path.resolve('test/server/data/test-suites/require-error-in-dep/dep.js'); + + const stack = [ + dep, + testfile, + ]; return compile(testfile) .then(function () { @@ -436,20 +913,18 @@ describe('Compiler', function () { }) .catch(function (err) { assertError(err, { - stackTop: [ - dep, - testfile - ], + stackTop: stack, - message: 'Cannot prepare tests due to an error.\n\n' + - "Error: Cannot find module './yo'" - }); + message: 'Cannot prepare tests due to the following error:\n\n' + + "Error: Cannot find module './yo'", + + }, true); }); }); it('Should raise an error if dependency throws runtime error', function () { - var testfile = resolve('test/server/data/test-suites/runtime-error-in-dep/testfile.js'); - var dep = resolve('test/server/data/test-suites/runtime-error-in-dep/dep.js'); + const testfile = path.resolve('test/server/data/test-suites/runtime-error-in-dep/testfile.js'); + const dep = path.resolve('test/server/data/test-suites/runtime-error-in-dep/dep.js'); return compile(testfile) .then(function () { @@ -459,17 +934,17 @@ describe('Compiler', function () { assertError(err, { stackTop: [ dep, - testfile + testfile, ], - message: 'Cannot prepare tests due to an error.\n\n' + - 'Error: Hey ya!' + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Error: Hey ya!', }); }); }); it("Should raise an error if test file can't require a module", function () { - var testfile = resolve('test/server/data/test-suites/require-error-in-testfile/testfile.js'); + const testfile = path.resolve('test/server/data/test-suites/require-error-in-testfile/testfile.js'); return compile(testfile) .then(function () { @@ -479,14 +954,14 @@ describe('Compiler', function () { assertError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - "Error: Cannot find module './yo'" - }); + message: 'Cannot prepare tests due to the following error:\n\n' + + "Error: Cannot find module './yo'", + }, true); }); }); it('Should raise an error if test file throws runtime error', function () { - var testfile = resolve('test/server/data/test-suites/runtime-error-in-testfile/testfile.js'); + const testfile = path.resolve('test/server/data/test-suites/runtime-error-in-testfile/testfile.js'); return compile(testfile) .then(function () { @@ -496,14 +971,14 @@ describe('Compiler', function () { assertError(err, { stackTop: testfile, - message: 'Cannot prepare tests due to an error.\n\n' + - 'Error: Hey ya!' + message: 'Cannot prepare tests due to the following error:\n\n' + + 'Error: Hey ya!', }); }); }); it('Should raise an error if test file has a syntax error', function () { - var testfile = posixResolve('test/server/data/test-suites/syntax-error-in-testfile/testfile.js'); + const testfile = path.resolve('test/server/data/test-suites/syntax-error-in-testfile/testfile.js'); return compile(testfile) .then(function () { @@ -513,47 +988,48 @@ describe('Compiler', function () { assertError(err, { stackTop: null, - message: 'Cannot prepare tests due to an error.\n\n' + - 'SyntaxError: ' + testfile + ': Unexpected token, expected { (1:7)' - }); + message: 'Cannot prepare tests due to the following error:\n\n' + + 'SyntaxError: ' + testfile + ": Unexpected keyword 'export'. (1:7)", + }, true); }); }); it('Should raise an error if test file has Flow syntax without a marker comment', function () { - var testfiles = [ - posixResolve('test/server/data/test-suites/flow-type-declarations/no-flow-marker.js'), - posixResolve('test/server/data/test-suites/flow-type-declarations/flower-marker.js') + const testfiles = [ + path.resolve('test/server/data/test-suites/flow-type-declarations/no-flow-marker.js'), + path.resolve('test/server/data/test-suites/flow-type-declarations/flower-marker.js'), ]; return compile(testfiles[0]) - .then(function () { + .then(() => { throw new Error('Promise rejection expected'); }) - .catch(function (err) { + .catch(err => { assertError(err, { stackTop: null, - message: 'Cannot prepare tests due to an error.\n\n' + - 'SyntaxError: ' + testfiles[0] + ': Unexpected token, expected ; (1:8)' - }); + + message: 'Cannot prepare tests due to the following error:\n\n' + + 'SyntaxError: ' + testfiles[0] + ': Missing semicolon. (1:7)', + }, true); return compile(testfiles[1]); }) - .then(function () { + .then(() => { throw new Error('Promise rejection expected'); }) - .catch(function (err) { + .catch(err => { assertError(err, { stackTop: null, - message: 'Cannot prepare tests due to an error.\n\n' + - 'SyntaxError: ' + testfiles[1] + ': Unexpected token, expected ; (2:8)' - }); + message: 'Cannot prepare tests due to the following error:\n\n' + + 'SyntaxError: ' + testfiles[1] + ': Missing semicolon. (2:7)', + }, true); }); }); it('Should raise an error if test file has a TypeScript error', function () { - var testfile = posixResolve('test/server/data/test-suites/typescript-compile-errors/testfile.ts'); + const testfile = posixResolve('test/server/data/test-suites/typescript-compile-errors/testfile.ts'); return compile(testfile) .then(function () { @@ -563,33 +1039,17 @@ describe('Compiler', function () { assertError(err, { stackTop: null, - message: 'Cannot prepare tests due to an error.\n\n' + + message: 'Cannot prepare tests due to the following error:\n\n' + 'Error: TypeScript compilation failed.\n' + testfile + ' (6, 13): Property \'doSmthg\' does not exist on type \'TestController\'.\n' + - testfile + ' (9, 6): Argument of type \'123\' is not assignable to parameter of type \'string\'.\n' + testfile + ' (9, 6): Argument of type \'number\' is not assignable to parameter of type \'string\'.\n' + + testfile + ' (18, 5): Unable to resolve signature of property decorator when called as an expression.\n', }); }); }); - }); - describe('Regression', function () { - it('Incorrect callsite line in error report on node v0.10.41 (GH-599)', function () { - return compile('test/server/data/test-suites/regression-gh-599/testfile.js') - .then(function (compiled) { - return compiled.tests[0].fn(testRunMock); - }) - .then(function () { - throw 'Promise rejection expected'; - }) - .catch(function (errList) { - var callsite = errList.items[0].callsite.renderSync({ renderer: renderers.noColor }); - - expect(callsite).contains(' > 19 | .method1()\n'); - }); - }); - it('Should successfully compile tests if re-export is used', function () { return compile('test/server/data/test-suites/regression-gh-969/testfile.js') .then(function (compiled) { @@ -599,21 +1059,33 @@ describe('Compiler', function () { it('Incorrect callsite stack in error report if "import" is used (GH-1226)', function () { return compile('test/server/data/test-suites/regression-gh-1226/testfile.js') - .then(function (compiled) { + .then(compiled => { return compiled.tests[0].fn(testRunMock); }) - .then(function () { + .then(() => { throw 'Promise rejection expected'; }) - .catch(function (errList) { - var stackTraceLimit = 200; - var err = errList.items[0]; - var stack = err.callsite.stackFrames.filter(createStackFilter(stackTraceLimit)); - - expect(stack.length).eql(3); - expect(stack[0].source).to.have.string('helper.js'); - expect(stack[1].source).to.have.string('helper.js'); - expect(stack[2].source).to.have.string('testfile.js'); + .catch(errList => { + const stackTraceLimit = 200; + const err = errList.items[0]; + const stack = err.callsite.stackFrames.filter(createStackFilter(stackTraceLimit)); + + expect(stack.length).eql(8); + + const lastStackItem = stack.pop(); + + stack.forEach(stackItem => { + expect(stackItem.source).to.have.string('helper.js'); + }); + + expect(lastStackItem.source).to.have.string('testfile.js'); + }); + }); + + it('getTests method should not raise an error for typescript declaration files', function () { + return compile('test/server/data/test-suites/typescript-description-file/fs.d.ts') + .then(res => { + expect(res).eql({ tests: [], fixtures: [] }); }); }); }); diff --git a/test/server/configuration-test.js b/test/server/configuration-test.js new file mode 100644 index 00000000000..6f41c35b30e --- /dev/null +++ b/test/server/configuration-test.js @@ -0,0 +1,1005 @@ +/*eslint-disable no-console */ +const { cloneDeep, noop } = require('lodash'); + +const { expect } = require('chai'); +const fs = require('fs'); +const path = require('path'); +const tmp = require('tmp'); +const { nanoid } = require('nanoid'); +const del = require('del'); +const pathUtil = require('path'); +const proxyquire = require('proxyquire'); + +const TestCafeConfiguration = proxyquire('../../lib/configuration/testcafe-configuration', { + './utils': { + getValidHostname: hostname => hostname || 'calculated-hostname', + }, +}); + +const TypeScriptConfiguration = require('../../lib/configuration/typescript-configuration'); +const { DEFAULT_TYPESCRIPT_COMPILER_OPTIONS } = require('../../lib/configuration/default-values'); +const RunnerCtor = require('../../lib/runner'); +const OptionNames = require('../../lib/configuration/option-names'); +const consoleWrapper = require('./helpers/console-wrapper'); +const Extensions = require('../../lib/configuration/formats'); + +const tsConfigPath = 'tsconfig.json'; +const customTSConfigFilePath = 'custom-config.json'; + +const createJSONConfig = (filePath, options) => { + options = options || {}; + fs.writeFileSync(filePath, JSON.stringify(options)); +}; + +const createJsConfig = (filePath, options) => { + options = options || {}; + fs.writeFileSync(filePath, `module.exports = ${JSON.stringify(options)}`); +}; + +const jsConfigIndex = TestCafeConfiguration.FILENAMES.findIndex(file=>file.includes(Extensions.js)); +const jsonConfigIndex = TestCafeConfiguration.FILENAMES.findIndex(file=>file.includes(Extensions.json)); + +const createJsTestCafeConfigurationFile = createJsConfig.bind(null, TestCafeConfiguration.FILENAMES[jsConfigIndex]); +const createJSONTestCafeConfigurationFile = createJSONConfig.bind(null, TestCafeConfiguration.FILENAMES[jsonConfigIndex]); +const createTypeScriptConfigurationFile = createJSONConfig.bind(null, tsConfigPath); + +const TEST_TIMEOUT = 5000; + +describe('TestCafeConfiguration', function () { + this.timeout(TEST_TIMEOUT); + + const testCafeConfiguration = new TestCafeConfiguration(); + let keyFileContent = null; + let keyFile = null; + + consoleWrapper.init(); + tmp.setGracefulCleanup(); + + beforeEach(() => { + keyFile = tmp.fileSync(); + keyFileContent = Buffer.from(nanoid()); + + fs.writeFileSync(keyFile.name, keyFileContent); + }); + + afterEach(async () => { + await del(testCafeConfiguration.defaultPaths); + + consoleWrapper.unwrap(); + consoleWrapper.messages.clear(); + }); + + describe('Basic', function () { + beforeEach(() => { + createJSONTestCafeConfigurationFile({ + 'hostname': '123.456.789', + 'port1': 1234, + 'port2': 5678, + 'src': 'path1/folder', + 'ssl': { + 'key': keyFile.name, + 'rejectUnauthorized': 'true', + }, + 'browsers': 'remote', + 'concurrency': 0.5, + 'filter': { + 'fixture': 'testFixture', + 'test': 'some test', + 'testGrep': 'test\\d', + 'fixtureGrep': 'fixture\\d', + 'testMeta': { test: 'meta' }, + 'fixtureMeta': { fixture: 'meta' }, + }, + 'clientScripts': 'test-client-script.js', + 'disableHttp2': true, + 'nativeAutomation': true, + 'baseUrl': 'localhost:3000', + 'skipJsErrors': { message: '/testRegex/i', stack: 'testRegex' }, + }); + }); + + describe('Init', () => { + describe('Exists', () => { + it('Config is not well-formed', async () => { + const filePath = testCafeConfiguration.defaultPaths[jsonConfigIndex]; + let message = ''; + + fs.writeFileSync(filePath, '{'); + + try { + await testCafeConfiguration.init(); + } + catch (err) { + message = err.message; + } + + expect(message).eql( + `Failed to parse the "${filePath}" configuration file. The file contains invalid JSON syntax. \n\nError details:\n\n` + + `JSON5: invalid end of input at 1:2` + ); + }); + + it('Non-existing module has been imported in config file', async () => { + const filePath = testCafeConfiguration.defaultPaths[jsConfigIndex]; + let message = ''; + + fs.writeFileSync(filePath, 'const f=require("fake");module.exports={}'); + + try { + await testCafeConfiguration.init(); + } + catch (err) { + message = err.message; + } + + expect(message).eql( + `Failed to read the "${filePath}" configuration file from disk. Error details:\n\n` + + `Cannot find module 'fake'\n` + + `Require stack:\n` + + `- ${filePath}` + ); + }); + + it('Non-existing module has been imported config file(depth 2)', async () => { + const filePath = path.resolve('./test/server/data/configuration/module-not-found/config.js'); + const modulePath = path.resolve('./test/server/data/configuration/module-not-found/module.js'); + let message = ''; + + const tcConfiguration = new TestCafeConfiguration(filePath); + + try { + await tcConfiguration.init(); + } + catch (err) { + message = err.message; + } + + expect(message).eql( + `Failed to read the "${filePath}" configuration file from disk. Error details:\n\n` + + `Cannot find module 'non-existing-module'\n` + + `Require stack:\n` + + `- ${modulePath}\n` + + `- ${filePath}` + ); + }); + + it('Options', () => { + return testCafeConfiguration.init() + .then(() => { + expect(testCafeConfiguration.getOption('hostname')).eql('123.456.789'); + expect(testCafeConfiguration.getOption('port1')).eql(1234); + + const ssl = testCafeConfiguration.getOption('ssl'); + + expect(ssl.key).eql(keyFileContent); + expect(ssl.rejectUnauthorized).eql(true); + expect(testCafeConfiguration.getOption('src')).eql(['path1/folder']); + expect(testCafeConfiguration.getOption('browsers')).to.be.an('array').that.not.empty; + expect(testCafeConfiguration.getOption('browsers')[0]).to.include({ providerName: 'remote' }); + expect(testCafeConfiguration.getOption('concurrency')).eql(0.5); + expect(testCafeConfiguration.getOption('filter')).to.be.a('function'); + expect(testCafeConfiguration.getOption('filter').testGrep.test('test1')).to.be.true; + expect(testCafeConfiguration.getOption('filter').fixtureGrep.test('fixture1')).to.be.true; + expect(testCafeConfiguration.getOption('filter').testMeta).to.be.deep.equal({ test: 'meta' }); + expect(testCafeConfiguration.getOption('filter').fixtureMeta).to.be.deep.equal({ fixture: 'meta' }); + expect(testCafeConfiguration.getOption('clientScripts')).eql([ 'test-client-script.js' ]); + expect(testCafeConfiguration.getOption('disableHttp2')).to.be.true; + expect(testCafeConfiguration.getOption('nativeAutomation')).to.be.true; + expect(testCafeConfiguration.getOption('baseUrl')).eql('localhost:3000'); + expect(testCafeConfiguration.getOption('skipJsErrors')).to.be.deep.equal({ message: '/testRegex/i', stack: 'testRegex' }); + }); + }); + + it('"Reporter" option', () => { + let optionValue = null; + + createJSONTestCafeConfigurationFile({ + reporter: 'json', + }); + + return testCafeConfiguration + .init() + .then(() => { + optionValue = testCafeConfiguration.getOption('reporter'); + + expect(optionValue.length).eql(1); + expect(optionValue[0].name).eql('json'); + + createJSONTestCafeConfigurationFile({ + reporter: ['json', 'minimal'], + }); + + return testCafeConfiguration.init(); + }) + .then(() => { + optionValue = testCafeConfiguration.getOption('reporter'); + + expect(optionValue.length).eql(2); + expect(optionValue[0].name).eql('json'); + expect(optionValue[1].name).eql('minimal'); + + createJSONTestCafeConfigurationFile({ + reporter: [ { + name: 'json', + file: 'path/to/file', + }], + }); + + return testCafeConfiguration.init(); + }) + .then(() => { + optionValue = testCafeConfiguration.getOption('reporter'); + + expect(optionValue.length).eql(1); + expect(optionValue[0].name).eql('json'); + expect(optionValue[0].file).eql('path/to/file'); + }); + }); + + describe('Screenshot options', () => { + it('`mergeOptions` overrides config values', async () => { + createJSONTestCafeConfigurationFile({ + 'screenshots': { + 'path': 'screenshot-path', + 'pathPattern': 'screenshot-path-pattern', + 'takeOnFails': true, + 'fullPage': true, + 'thumbnails': true, + }, + }); + + await testCafeConfiguration.init(); + + testCafeConfiguration.mergeOptions({ + screenshots: { + path: 'modified-path', + pathPattern: 'modified-pattern', + takeOnFails: false, + fullPage: false, + thumbnails: false, + }, + }); + + expect(testCafeConfiguration.getOption('screenshots')).eql({ + path: 'modified-path', + pathPattern: 'modified-pattern', + takeOnFails: false, + fullPage: false, + thumbnails: false, + }); + + expect(testCafeConfiguration._overriddenOptions).eql([ + 'screenshots.path', + 'screenshots.pathPattern', + 'screenshots.takeOnFails', + 'screenshots.fullPage', + 'screenshots.thumbnails', + ]); + }); + + it('`mergeOptions` merges config values', async () => { + createJSONTestCafeConfigurationFile({ + 'screenshots': { + 'path': 'screenshot-path', + 'pathPattern': 'screenshot-path-pattern', + }, + }); + + await testCafeConfiguration.init(); + + testCafeConfiguration.mergeOptions({ + screenshots: { + path: 'modified-path', + pathPattern: void 0, + takeOnFails: false, + }, + }); + + expect(testCafeConfiguration.getOption('screenshots')).eql({ + path: 'modified-path', + pathPattern: 'screenshot-path-pattern', + takeOnFails: false, + }); + expect(testCafeConfiguration._overriddenOptions).eql(['screenshots.path']); + }); + + it('`mergeOptions` with an empty object does not override anything', async () => { + createJSONTestCafeConfigurationFile({ + 'screenshots': { + 'path': 'screenshot-path', + 'pathPattern': 'screenshot-path-pattern', + }, + }); + + await testCafeConfiguration.init(); + + testCafeConfiguration.mergeOptions({ }); + testCafeConfiguration.mergeOptions({ screenshots: {} }); + + expect(testCafeConfiguration.getOption('screenshots')).eql({ + path: 'screenshot-path', + pathPattern: 'screenshot-path-pattern', + }); + }); + + it('both `screenshots` options exist in config', async () => { + createJSONTestCafeConfigurationFile({ + 'screenshots': { + 'path': 'screenshot-path-1', + 'pathPattern': 'screenshot-path-pattern-1', + 'takeOnFails': true, + 'fullPage': true, + 'thumbnails': true, + }, + 'screenshotPath': 'screenshot-path-2', + 'screenshotPathPattern': 'screenshot-path-pattern-2', + 'takeScreenshotsOnFails': false, + }); + + await testCafeConfiguration.init(); + + expect(testCafeConfiguration._overriddenOptions.length).eql(0); + expect(testCafeConfiguration.getOption('screenshots')).eql({ + path: 'screenshot-path-1', + pathPattern: 'screenshot-path-pattern-1', + takeOnFails: true, + fullPage: true, + thumbnails: true, + }); + expect(testCafeConfiguration.getOption('screenshotPath')).eql('screenshot-path-2'); + expect(testCafeConfiguration.getOption('screenshotPathPattern')).eql('screenshot-path-pattern-2'); + expect(testCafeConfiguration.getOption('takeScreenshotsOnFails')).eql(false); + }); + }); + + it("Shouldn't change filter option with function", async () => { + fs.writeFileSync(TestCafeConfiguration.FILENAMES[jsConfigIndex], `module.exports = {filter: (testName, fixtureName, fixturePath, testMeta, fixtureMeta) => true}`); + + await testCafeConfiguration.init(); + + expect(testCafeConfiguration.getOption('filter')).to.be.a('function'); + expect(testCafeConfiguration.getOption('filter')()).to.be.true; + }); + + it('Should warn message on multiple configuration files', async () => { + createJsTestCafeConfigurationFile({ + 'hostname': '123.456.789', + 'port1': 1234, + 'port2': 5678, + 'src': 'path1/folder', + 'browser': 'edge', + }); + + consoleWrapper.wrap(); + await testCafeConfiguration.init(); + consoleWrapper.unwrap(); + + const expectedMessage = + `TestCafe detected more than one configuration file.\n` + + `To prevent configuration conflicts, TestCafe will use the configuration file with the highest priority: ${pathUtil.resolve('.testcaferc.js')}.\n` + + `Refer to the following article for more information: https://testcafe.io/documentation/402638/reference/configuration-file?search#configuration-file-priority`; + + expect(consoleWrapper.messages.log).eql(expectedMessage); + }); + + it('Should read JS config file if JSON and JS default files exist', async () => { + createJsTestCafeConfigurationFile({ + 'jsConfig': true, + }); + createJSONTestCafeConfigurationFile({ + 'jsConfig': false, + }); + + await testCafeConfiguration.init(); + + expect(testCafeConfiguration.getOption('jsConfig')).to.be.true; + }); + }); + + it("File doesn't exists", async () => { + fs.unlinkSync(TestCafeConfiguration.FILENAMES[jsonConfigIndex]); + + const defaultOptions = cloneDeep(testCafeConfiguration._options); + + await testCafeConfiguration.init(); + + expect(testCafeConfiguration._options).to.deep.equal(defaultOptions); + }); + + it('Explicitly specified configuration file doesn\'t exist', async () => { + let message = null; + + const nonExistingConfiguration = new TestCafeConfiguration('non-existing-path'); + + try { + await nonExistingConfiguration.init(); + } + catch (err) { + message = err.message; + } + + expect(message).contains(`Cannot locate a TestCafe configuration file at ${nonExistingConfiguration.filePath}`); + }); + }); + + describe('Merge options', () => { + it('One', () => { + consoleWrapper.wrap(); + + return testCafeConfiguration.init() + .then(() => { + testCafeConfiguration.mergeOptions({ 'hostname': 'anotherHostname' }); + testCafeConfiguration.notifyAboutOverriddenOptions(); + + consoleWrapper.unwrap(); + + expect(testCafeConfiguration.getOption('hostname')).eql('anotherHostname'); + expect(consoleWrapper.messages.log).eql('The "hostname" option from the configuration file will be ignored.'); + }); + }); + + it('Many', () => { + consoleWrapper.wrap(); + + return testCafeConfiguration.init() + .then(() => { + testCafeConfiguration.mergeOptions({ + 'hostname': 'anotherHostname', + 'port1': 'anotherPort1', + 'port2': 'anotherPort2', + }); + + testCafeConfiguration.notifyAboutOverriddenOptions(); + + consoleWrapper.unwrap(); + + expect(testCafeConfiguration.getOption('hostname')).eql('anotherHostname'); + expect(testCafeConfiguration.getOption('port1')).eql('anotherPort1'); + expect(testCafeConfiguration.getOption('port2')).eql('anotherPort2'); + expect(consoleWrapper.messages.log).eql('The "hostname", "port1", and "port2" options from the configuration file will be ignored.'); + }); + }); + + it('Should ignore an option with the "undefined" value', () => { + return testCafeConfiguration.init() + .then(() => { + testCafeConfiguration.mergeOptions({ 'hostname': void 0 }); + + expect(testCafeConfiguration.getOption('hostname')).eql('123.456.789'); + }); + }); + + describe('Calculate hostname', () => { + let configuration; + + beforeEach(async () => { + configuration = new TestCafeConfiguration(); + + await del(configuration.defaultPaths); + }); + + it('Native automation is enabled/hostname is unset', async () => { + await configuration.init(); + await configuration.calculateHostname({ nativeAutomation: true }); + + expect(configuration.getOption('hostname')).eql('localhost'); + }); + + it('Native automation is enabled/hostname is set', async () => { + await configuration.init({ hostname: '123.456.789' }); + await configuration.calculateHostname({ nativeAutomation: true }); + + expect(configuration.getOption('hostname')).eql('localhost'); + }); + + it('Native automation is disabled/hostname is unset', async () => { + await configuration.init(); + await configuration.calculateHostname({ nativeAutomation: false }); + + expect(configuration.getOption('hostname')).eql('calculated-hostname'); + }); + + it('Native automation is disabled/hostname is set', async () => { + await configuration.init({ hostname: '123.456.789' }); + await configuration.calculateHostname({ nativeAutomation: false }); + + expect(configuration.getOption('hostname')).eql('123.456.789'); + }); + }); + }); + + describe('Should copy value from "tsConfigPath" to compiler options', () => { + it('only tsConfigPath is specified', async () => { + const configuration = new TestCafeConfiguration(); + const runner = new RunnerCtor({ configuration }); + + await runner.tsConfigPath('path-to-ts-config'); + await runner._setConfigurationOptions(); + await runner._setBootstrapperOptions(); + + expect(runner.configuration.getOption(OptionNames.compilerOptions)).eql({ + 'typescript': { + configPath: 'path-to-ts-config', + }, + }); + }); + + it('tsConfigPath is specified and compiler options are "undefined"', async () => { + const configuration = new TestCafeConfiguration(); + const runner = new RunnerCtor({ configuration }); + + await runner + .tsConfigPath('path-to-ts-config') + .compilerOptions(void 0); // emulate command-line run + await runner._setConfigurationOptions(); + await runner._setBootstrapperOptions(); + + expect(runner.configuration.getOption(OptionNames.compilerOptions)).eql({ + 'typescript': { + configPath: 'path-to-ts-config', + }, + }); + }); + + it('both "tsConfigPath" and compiler options are specified', async () => { + const configuration = new TestCafeConfiguration(); + const runner = new RunnerCtor({ configuration }); + + await runner + .tsConfigPath('path-to-ts-config') + .compilerOptions({ + 'typescript': { + configPath: 'path-in-compiler-options', + }, + }); + await runner._setConfigurationOptions(); + await runner._setBootstrapperOptions(); + + expect(runner.configuration.getOption(OptionNames.compilerOptions)).eql({ + 'typescript': { + configPath: 'path-in-compiler-options', + }, + }); + }); + }); + + describe('Clone', () => { + it('Should clone configuration', async () => { + const configuration = new TestCafeConfiguration(); + + await configuration.init({ + userVariables: { + url: 'localhos', + }, + }); + + const clone = configuration.clone(); + + expect(configuration.getOption('userVariables')).not.equal(clone.getOption('userVariables')); + }); + + it('Should clone configuration except an "hooks" option', async () => { + const configuration = new TestCafeConfiguration(); + + await configuration.init({ + hooks: { + request: 'request', + }, + userVariables: { + url: 'localhost', + }, + }); + + const clone = configuration.clone(OptionNames.hooks); + + expect(configuration.getOption('userVariables')).not.equal(clone.getOption('userVariables')); + expect(configuration.getOption('hooks')).to.equal(clone.getOption('hooks')); + }); + }); + + describe('[API] CustomActions', () => { + it('Should get custom actions from the JS Configuration file property', async () => { + const customConfigFilePath = './test/server/data/custom-actions/config.js'; + + const configuration = new TestCafeConfiguration(customConfigFilePath); + + await configuration.init(); + + const customActions = configuration.getOption('customActions'); + + expect(customActions.makeSomething).to.be.a('function'); + expect(customActions.doSomething).to.be.a('function'); + }); + }); + }); + + describe('Default values', function () { + beforeEach(() => { + createJSONTestCafeConfigurationFile({ }); + }); + + async function testInitWithMergeOption (opts) { + consoleWrapper.wrap(); + + await testCafeConfiguration.init(); + + consoleWrapper.unwrap(); + + testCafeConfiguration.mergeOptions(opts); + } + + it('Filter', async () => { + await testInitWithMergeOption({ filter: noop }); + + expect(testCafeConfiguration._overriddenOptions).eql([]); + }); + + it('Init flags', async () => { + await testInitWithMergeOption({ developmentMode: true }); + + expect(testCafeConfiguration._overriddenOptions).eql([]); + }); + }); + + describe('[RG-6618] Incorrect browser is specified in config file when running tests from CLI', () => { + let configuration; + const customConfigFile = 'custom2.testcaferc.json'; + + const options = { + 'browsers': ['incorrectBrowser'], + }; + + before(async () => { + createJSONConfig(customConfigFile, options); + }); + + after(async () => { + await del(configuration.defaultPaths); + }); + + it('Should success create configuration with incorrect browser value', () => { + configuration = new TestCafeConfiguration(customConfigFile); + + return configuration.init({ isCli: true }) + .then(() => { + expect(pathUtil.basename(configuration.filePath)).eql(customConfigFile); + expect(configuration.getOption('browsers')).eql(options.browsers); + }); + }); + + it('Should throw an error in case of incorrect browser was passed not from CLI', () => { + configuration = new TestCafeConfiguration(customConfigFile); + + return configuration.init().then(() => { + throw new Error('Promise should be rejected'); + }).catch(err => { + expect(err.message).eql('Cannot find the browser. "incorrectBrowser" is neither a known browser alias, nor a path to an executable file.'); + }); + }); + }); +}); + +describe('TypeScriptConfiguration', function () { + this.timeout(TEST_TIMEOUT); + + const typeScriptConfiguration = new TypeScriptConfiguration(tsConfigPath); + + it('Default', () => { + const defaultTypeScriptConfiguration = new TypeScriptConfiguration(); + + return defaultTypeScriptConfiguration.init() + .then(() => { + expect(defaultTypeScriptConfiguration.getOptions()).to.deep.equal(DEFAULT_TYPESCRIPT_COMPILER_OPTIONS); + }); + }); + + it('Configuration file does not exist', async () => { + let message = null; + + const nonExistingConfiguration = new TypeScriptConfiguration('non-existing-path'); + + try { + await nonExistingConfiguration.init(); + } + catch (err) { + message = err.message; + } + + expect(message).eql(`"${nonExistingConfiguration.defaultPaths[jsConfigIndex]}" is not a valid TypeScript configuration file.`); + }); + + it('Config is not well-formed', async () => { + let message = ''; + + fs.writeFileSync(tsConfigPath, '{'); + + try { + await typeScriptConfiguration.init(); + } + catch (err) { + message = err.message; + } + + fs.unlinkSync(tsConfigPath); + + expect(message).eql( + `Failed to parse the "${typeScriptConfiguration.filePath}" configuration file. The file contains invalid JSON syntax. \n\n` + + `Error details:\n\n` + + `JSON5: invalid end of input at 1:2` + ); + }); + + describe('With configuration file', () => { + tmp.setGracefulCleanup(); + + beforeEach(() => { + consoleWrapper.init(); + consoleWrapper.wrap(); + }); + + afterEach(async () => { + await del(typeScriptConfiguration.defaultPaths.concat(customTSConfigFilePath)); + + consoleWrapper.unwrap(); + consoleWrapper.messages.clear(); + }); + + it('tsconfig.json does not apply automatically', () => { + const defaultTSConfiguration = new TypeScriptConfiguration(); + + createTypeScriptConfigurationFile({ + compilerOptions: { + experimentalDecorators: false, + }, + }); + + return defaultTSConfiguration.init() + .then(() => { + consoleWrapper.unwrap(); + + const options = defaultTSConfiguration.getOptions(); + + expect(options['experimentalDecorators']).eql(true); + }); + }); + + it('override options', () => { + // NOTE: suppressOutputPathCheck can't be overridden by a config file + createTypeScriptConfigurationFile({ + compilerOptions: { + experimentalDecorators: false, + emitDecoratorMetadata: false, + allowJs: false, + pretty: false, + inlineSourceMap: false, + noImplicitAny: true, + + module: 'esnext', + moduleResolution: 'classic', + target: 'esnext', + lib: ['es2018', 'dom'], + + incremental: true, + tsBuildInfoFile: 'tsBuildInfo.txt', + emitDeclarationOnly: true, + declarationMap: true, + declarationDir: 'C:/', + composite: true, + outFile: 'oufile.js', + out: '', + }, + }); + + return typeScriptConfiguration.init() + .then(() => { + consoleWrapper.unwrap(); + + const options = typeScriptConfiguration.getOptions(); + + expect(options['experimentalDecorators']).eql(false); + expect(options['emitDecoratorMetadata']).eql(false); + expect(options['allowJs']).eql(false); + expect(options['pretty']).eql(false); + expect(options['inlineSourceMap']).eql(false); + expect(options['noImplicitAny']).eql(true); + expect(options['suppressOutputPathCheck']).eql(true); + + // NOTE: `module` and `target` default options can not be overridden by custom config + expect(options['module']).eql(1); + expect(options['moduleResolution']).eql(2); + expect(options['target']).eql(3); + + expect(options['lib']).deep.eql(['lib.es2018.d.ts', 'lib.dom.d.ts']); + + expect(options).not.have.property('incremental'); + expect(options).not.have.property('tsBuildInfoFile'); + expect(options).not.have.property('emitDeclarationOnly'); + expect(options).not.have.property('declarationMap'); + expect(options).not.have.property('declarationDir'); + expect(options).not.have.property('composite'); + expect(options).not.have.property('outFile'); + expect(options).not.have.property('out'); + + expect(consoleWrapper.messages.log).contains('You cannot override the "module" compiler option in the TypeScript configuration file.'); + expect(consoleWrapper.messages.log).contains('You cannot override the "moduleResolution" compiler option in the TypeScript configuration file.'); + expect(consoleWrapper.messages.log).contains('You cannot override the "target" compiler option in the TypeScript configuration file.'); + }); + }); + + it('Should not display override messages if config values are the same as default values', () => { + const tsConfiguration = new TypeScriptConfiguration(tsConfigPath); + + createTypeScriptConfigurationFile({ + compilerOptions: { + module: 'commonjs', + moduleResolution: 'node', + target: 'es2016', + }, + }); + + return tsConfiguration.init() + .then(() => { + consoleWrapper.unwrap(); + + expect(consoleWrapper.messages.log).not.ok; + }); + }); + + it('TestCafe config + TypeScript config', async function () { + createJSONTestCafeConfigurationFile({ + tsConfigPath: customTSConfigFilePath, + }); + + createJSONConfig(customTSConfigFilePath, { + compilerOptions: { + target: 'es5', + }, + }); + + const configuration = new TestCafeConfiguration(); + const runner = new RunnerCtor({ configuration }); + + await configuration.init(); + await runner.src('test/server/data/test-suites/typescript-basic/testfile1.ts'); + await runner._setConfigurationOptions(); + await runner._setBootstrapperOptions(); + await runner.bootstrapper._getTests(); + + fs.unlinkSync(TestCafeConfiguration.FILENAMES[jsonConfigIndex]); + typeScriptConfiguration._filePath = customTSConfigFilePath; + + expect(runner.bootstrapper.tsConfigPath).eql(customTSConfigFilePath); + expect(consoleWrapper.messages.log).contains('You cannot override the "target" compiler option in the TypeScript configuration file.'); + }); + + describe('Should warn message on rewrite a non-overridable property', () => { + it('TypeScript config', async function () { + let runner = null; + + createJSONConfig(customTSConfigFilePath, { + compilerOptions: { + target: 'es5', + }, + }); + + runner = new RunnerCtor({ configuration: new TestCafeConfiguration() }); + + runner.src('test/server/data/test-suites/typescript-basic/testfile1.ts'); + runner.tsConfigPath(customTSConfigFilePath); + runner._setBootstrapperOptions(); + + await runner._setConfigurationOptions(); + await runner._setBootstrapperOptions(); + await runner.bootstrapper._getTests(); + + typeScriptConfiguration._filePath = customTSConfigFilePath; + + expect(runner.bootstrapper.tsConfigPath).eql(customTSConfigFilePath); + expect(consoleWrapper.messages.log).contains('You cannot override the "target" compiler option in the TypeScript configuration file.'); + }); + + it('Custom compiler options', async () => { + const runner = new RunnerCtor({ configuration: new TestCafeConfiguration() }); + + runner + .src('test/server/data/test-suites/typescript-basic/testfile1.ts') + .compilerOptions({ + 'typescript': { + 'options': { target: 'es5' }, + }, + }); + + await runner._setConfigurationOptions(); + await runner._setBootstrapperOptions(); + await runner.bootstrapper._getTests(); + + expect(consoleWrapper.messages.log).contains('You cannot override the "target" compiler option in the TypeScript configuration file.'); + }); + }); + }); + + describe('Custom Testcafe Config Path', () => { + let configuration; + + afterEach(async () => { + await del(configuration.defaultPaths); + }); + + it('Custom config path is used', () => { + const customConfigFile = 'custom11.testcaferc.json'; + + const options = { + 'hostname': '123.456.789', + 'port1': 1234, + 'port2': 5678, + 'src': 'path1/folder', + 'browser': 'edge', + }; + + createJSONConfig(customConfigFile, options); + + configuration = new TestCafeConfiguration(customConfigFile); + + return configuration.init() + .then(() => { + expect(pathUtil.basename(configuration.filePath)).eql(customConfigFile); + expect(configuration.getOption('hostname')).eql(options.hostname); + expect(configuration.getOption('port1')).eql(options.port1); + expect(configuration.getOption('port2')).eql(options.port2); + expect(configuration.getOption('src')).eql([ options.src ]); + expect(configuration.getOption('browser')).eql(options.browser); + }); + }); + + it('Custom js config path is used', async () => { + const customConfigFile = 'custom11.testcaferc.js'; + + const options = { + 'hostname': '123.456.789', + 'port1': 1234, + 'port2': 5678, + 'src': 'path1/folder', + 'browser': 'edge', + }; + + createJsConfig(customConfigFile, options); + + configuration = new TestCafeConfiguration(customConfigFile); + + await configuration.init(); + + expect(pathUtil.basename(configuration.filePath)).eql(customConfigFile); + expect(configuration.getOption('hostname')).eql(options.hostname); + expect(configuration.getOption('port1')).eql(options.port1); + expect(configuration.getOption('port2')).eql(options.port2); + expect(configuration.getOption('src')).eql([ options.src ]); + expect(configuration.getOption('browser')).eql(options.browser); + }); + + it('Constructor should revert back to default when no custom config', () => { + const defaultFileLocation = '.testcaferc.json'; + + const options = { + 'hostname': '123.456.789', + 'port1': 1234, + 'port2': 5678, + 'src': 'path1/folder', + 'browser': 'edge', + }; + + createJSONConfig(defaultFileLocation, options); + + configuration = new TestCafeConfiguration(); + + return configuration.init() + .then(() => { + expect(pathUtil.basename(configuration.filePath)).eql(defaultFileLocation); + expect(configuration.getOption('hostname')).eql(options.hostname); + expect(configuration.getOption('port1')).eql(options.port1); + expect(configuration.getOption('port2')).eql(options.port2); + expect(configuration.getOption('src')).eql([ options.src ]); + expect(configuration.getOption('browser')).eql(options.browser); + }); + }); + }); +}); diff --git a/test/server/create-testcafe-test.js b/test/server/create-testcafe-test.js index 9e1afda9c4f..e5e38780b9b 100644 --- a/test/server/create-testcafe-test.js +++ b/test/server/create-testcafe-test.js @@ -1,18 +1,29 @@ -var expect = require('chai').expect; -var url = require('url'); -var net = require('net'); -var createTestCafe = require('../../lib/'); -var exportableLib = require('../../lib/api/exportable-lib'); -var Promise = require('pinkie'); +const { expect } = require('chai'); +const url = require('url'); +const net = require('net'); +const path = require('path'); +const proxyquire = require('proxyquire'); +const createTestCafe = require('../../lib/'); +const exportableLib = require('../../lib/api/exportable-lib'); +const selfSignedCertificate = require('openssl-self-signed-certificate'); +const Extensions = require('../../lib/configuration/formats'); +const TestCafeConfiguration = proxyquire('../../lib/configuration/testcafe-configuration', { + './utils': { + getValidHostname: hostname => hostname || 'calculated-hostname', + }, +}); + +const jsConfigIndex = TestCafeConfiguration.FILENAMES.findIndex(file=>file.includes(Extensions.js)); +const jsonConfigIndex = TestCafeConfiguration.FILENAMES.findIndex(file=>file.includes(Extensions.json)); describe('TestCafe factory function', function () { - var testCafe = null; - var server = null; + let testCafe = null; + let server = null; - function getTestCafe (hostname, port1, port2) { - return createTestCafe(hostname, port1, port2) - .then(function (tc) { + function getTestCafe (hostname, port1, port2, ssl, developmentMode, retryTestPages, cache, configPath) { + return createTestCafe(hostname, port1, port2, ssl, developmentMode, retryTestPages, cache, configPath) + .then(tc => { testCafe = tc; }); } @@ -20,12 +31,14 @@ describe('TestCafe factory function', function () { afterEach(function () { if (server) { server.close(); + server = null; } - var promisedClose = testCafe ? testCafe.close() : Promise.resolve(); + const promisedClose = testCafe ? testCafe.close() : Promise.resolve(); testCafe = null; + return promisedClose; }); @@ -35,12 +48,14 @@ describe('TestCafe factory function', function () { return testCafe.createBrowserConnection(); }) .then(function (bc) { - var bcUrl = url.parse(bc.url); - var port = parseInt(bcUrl.port, 10); + const bcUrl = url.parse(bc.url); + const port = parseInt(bcUrl.port, 10); expect(bcUrl.hostname).not.eql('undefined'); expect(bcUrl.hostname).not.eql('null'); expect(isNaN(port)).to.be.false; + + return bc.close(); }); }); @@ -50,16 +65,18 @@ describe('TestCafe factory function', function () { return testCafe.createBrowserConnection(); }) .then(function (bc) { - var bcUrl = url.parse(bc.url); - var port = parseInt(bcUrl.port, 10); + const bcUrl = url.parse(bc.url); + const port = parseInt(bcUrl.port, 10); expect(bcUrl.hostname).eql('localhost'); expect(port).eql(1338); + + return bc.close(); }); }); it('Should raise error if specified port is not free', function () { - var serverListen = new Promise(function (resolve) { + const serverListen = new Promise(function (resolve) { server = net.createServer(); server.listen(1337, resolve); @@ -73,24 +90,71 @@ describe('TestCafe factory function', function () { throw new Error('Promise rejection expected'); }) .catch(function (err) { - expect(err.message).eql('The specified 1337 port is already in use by another program.'); + expect(err.message).eql('Port 1337 is occupied by another process.'); }); }); it("Should raise error if specified hostname doesn't resolve to the current machine", function () { return getTestCafe('example.org') + .then(function () { + return testCafe.createBrowserConnection(); + }) .then(function () { throw new Error('Promise rejection expected'); }) .catch(function (err) { - expect(err.message).eql('The specified "example.org" hostname cannot be resolved to the current machine.'); + expect(err.message).eql('Cannot resolve hostname "example.org".'); }); }); - it('Should contain plugin testing, embedding utils and common runtime functions', function () { - expect(createTestCafe.pluginTestingUtils).to.be.an.object; - expect(createTestCafe.embeddingUtils).to.be.an.object; + it('Should contain embedding utils and common runtime functions', function () { + expect(createTestCafe.embeddingUtils).to.be.ok; expect(createTestCafe.Role).eql(exportableLib.Role); expect(createTestCafe.ClientFunction).eql(exportableLib.ClientFunction); }); + + it('Should pass sslOptions to proxy', () => { + const sslOptions = { + key: selfSignedCertificate.key, + cert: selfSignedCertificate.cert, + }; + + return getTestCafe('localhost', 1338, 1339, sslOptions) + .then(() => { + return testCafe.createBrowserConnection(); + }) + .then(() => { + expect(testCafe.proxy.server1.key).eql(sslOptions.key); + expect(testCafe.proxy.server1.cert).eql(sslOptions.cert); + expect(testCafe.proxy.server2.key).eql(sslOptions.key); + expect(testCafe.proxy.server2.cert).eql(sslOptions.cert); + }); + }); + + describe('Custom Testcafe Config Path', () => { + it('Reverts back to default when not specified', () => { + const defaultConfigJSONFile = '.testcaferc.json'; + const defaultConfigJsFile = '.testcaferc.js'; + + return getTestCafe('localhost', 1338, 1339) + .then(() => { + expect(path.basename(testCafe.configuration.defaultPaths[jsConfigIndex])).eql(defaultConfigJsFile); + expect(path.basename(testCafe.configuration.defaultPaths[jsonConfigIndex])).eql(defaultConfigJSONFile); + }); + }); + + it('Works when created using null', () => { + const defaultConfigJSONFile = '.testcaferc.json'; + const defaultConfigJsFile = '.testcaferc.js'; + + return createTestCafe(null) + .then(tc => { + testCafe = tc; + }) + .then(() => { + expect(path.basename(testCafe.configuration.defaultPaths[jsConfigIndex])).eql(defaultConfigJsFile); + expect(path.basename(testCafe.configuration.defaultPaths[jsonConfigIndex])).eql(defaultConfigJSONFile); + }); + }); + }); }); diff --git a/test/server/crop-test.js b/test/server/crop-test.js new file mode 100644 index 00000000000..c6ed186fd99 --- /dev/null +++ b/test/server/crop-test.js @@ -0,0 +1,153 @@ +const expect = require('chai').expect; + +const { + MARK_LENGTH, + MARK_BYTES_PER_PIXEL, + MARK_RIGHT_MARGIN, +} = require('../../lib/screenshots/constants'); + +const { + getClipInfoByCropDimensions, + calculateMarkPosition, + getClipInfoByMarkPosition, + calculateClipInfo, +} = require('../../lib/screenshots/crop'); + +const markSeed = Array(MARK_LENGTH).fill().flatMap((_, i) => i % 2 ? [255, 255, 255, 255] : [0, 0, 0, 255]); + +function getPngMock (mark = markSeed) { + const width = 1820; + const height = 954; + const length = width * height * MARK_BYTES_PER_PIXEL; + const markSeedOffset = (MARK_LENGTH + MARK_RIGHT_MARGIN) * MARK_BYTES_PER_PIXEL; + const markSeedIndex = length - markSeedOffset; + + const dataHeader = Buffer.from('-'.repeat(markSeedIndex)); + const dataMark = Buffer.from(mark); + const dataTrailer = Buffer.from('-'.repeat(MARK_RIGHT_MARGIN * MARK_BYTES_PER_PIXEL)); + + const data = Buffer.concat([dataHeader, dataMark, dataTrailer]); + + return { width, height, data }; +} + +describe('Crop images', () => { + it('Update clipInfo by crop dimensions', () => { + const clip = { + clipRight: 400, + clipLeft: 10, + clipBottom: 500, + clipTop: 10, + }; + + // left top + expect(getClipInfoByCropDimensions(clip, { left: 0, right: 380, top: 0, bottom: 480 })).eql({ + clipLeft: 10, + clipRight: 390, + clipTop: 10, + clipBottom: 490, + }); + + // right top + expect(getClipInfoByCropDimensions(clip, { left: 20, right: 420, top: 0, bottom: 480 })).eql({ + clipLeft: 30, + clipRight: 400, + clipTop: 10, + clipBottom: 490, + }); + + // right bottom + expect(getClipInfoByCropDimensions(clip, { left: 20, right: 420, top: 20, bottom: 520 })).eql({ + clipLeft: 30, + clipRight: 400, + clipTop: 30, + clipBottom: 500, + }); + + // left bottom + expect(getClipInfoByCropDimensions(clip, { left: 0, right: 380, top: 20, bottom: 520 })).eql({ + clipLeft: 10, + clipRight: 390, + clipTop: 30, + clipBottom: 500, + }); + + // middle + expect(getClipInfoByCropDimensions(clip, { left: 20, right: 360, top: 20, bottom: 460 })).eql({ + clipLeft: 30, + clipRight: 370, + clipTop: 30, + clipBottom: 470, + }); + }); + + it('Calculate mark position', () => { + expect(calculateMarkPosition(getPngMock(), markSeed)).eql({ + x: 1820, + y: 954, + }); + + expect(calculateMarkPosition(getPngMock(), '+')).eql(null); + }); + + it('Mark seed correction', () => { + const fixableMarkSeed = markSeed.slice(); + const spoiledMarkSeed = markSeed.slice(); + + fixableMarkSeed.splice(0, 1, 1); + spoiledMarkSeed.splice(0, 1, 10); + + expect(calculateMarkPosition(getPngMock(fixableMarkSeed), markSeed)).eql({ x: 1820, y: 954 }); + expect(calculateMarkPosition(getPngMock(spoiledMarkSeed), markSeed)).eql(null); + expect(calculateMarkPosition(getPngMock(), '+')).eql(null); + }); + + it('Get clipInfo by mark position', () => { + const markPosition = calculateMarkPosition(getPngMock(), markSeed); + + expect(getClipInfoByMarkPosition(markPosition, { width: 1820, height: 954 })).eql({ + clipLeft: 0, + clipRight: 1820, + clipTop: 0, + clipBottom: 954, + }); + + expect(getClipInfoByMarkPosition(markPosition, { width: 1620, height: 854 })).eql({ + clipLeft: 200, + clipRight: 1820, + clipTop: 100, + clipBottom: 954, + }); + }); + + it('Calculate clipInfo', () => { + const clientAreaDimensions = { + width: 1820, + height: 954, + }; + + const cropDimensions = { + left: 20, + right: 1800, + top: 20, + bottom: 850, + }; + + const pngImage = getPngMock(); + const markPosition = calculateMarkPosition(pngImage, markSeed); + + expect(calculateClipInfo(pngImage, markPosition, clientAreaDimensions)).eql({ + clipLeft: 0, + clipTop: 0, + clipRight: 1820, + clipBottom: 953, + }); + + expect(calculateClipInfo(pngImage, markPosition, clientAreaDimensions, cropDimensions)).eql({ + clipLeft: 20, + clipTop: 20, + clipRight: 1800, + clipBottom: 850, + }); + }); +}); diff --git a/test/server/custom-client-scripts-test.js b/test/server/custom-client-scripts-test.js new file mode 100644 index 00000000000..711f00b0490 --- /dev/null +++ b/test/server/custom-client-scripts-test.js @@ -0,0 +1,301 @@ +const { expect } = require('chai'); +const ClientScript = require('../../lib/custom-client-scripts/client-script'); +const loadClientScripts = require('../../lib/custom-client-scripts/load'); +const { RequestFilterRule } = require('testcafe-hammerhead'); +const tmp = require('tmp'); +const fs = require('fs'); +const { setUniqueUrls, findProblematicScripts } = require('../../lib/custom-client-scripts/utils'); +const { is } = require('../../lib/errors/runtime/type-assertions'); + +describe('Client scripts', () => { + tmp.setGracefulCleanup(); + + const testScriptContent = 'var i = 3;'; + const testModuleName = 'is-docker'; + const testBasePath = process.cwd(); + + function createScriptFile (content) { + const file = tmp.fileSync(); + + fs.writeFileSync(file.name, content || testScriptContent); + + return file; + } + + it('Should throw the error if script initializer is not specified', () => { + return new ClientScript().load() + .then(() => { + expect.fail('Should throw the error'); + }) + .catch(e => { + expect(e.message).eql('Initialize your client script with one of the following: a JavaScript script, a JavaScript file path, or the name of a JavaScript module.'); + }); + }); + + it('Should throw the error if scripts`s base path is not specified and path is relative', () => { + return new ClientScript({ path: './relative-path' }) + .load() + .then(() => { + expect.fail('Should throw the error'); + }) + .catch(e => { + expect(e.message).eql('Specify the base path for the client script file.'); + }); + }); + + describe('Content', () => { + it('Empty content', () => { + const script = new ClientScript({ content: '' }); + + return script + .load() + .then(() => { + expect(script.content).eql(''); + expect(script.toString()).eql('{ content: }'); + }); + }); + + it('Short content', () => { + const script = new ClientScript({ content: testScriptContent }); + + return script + .load() + .then(() => { + expect(script.content).eql(testScriptContent); + expect(script.url).is.not.empty; + expect(script.path).eql(null); + expect(script.toString()).eql("{ content: 'var i = 3;' }"); + }); + }); + + it('Long content', () => { + const script = new ClientScript({ content: testScriptContent.repeat(10) }); + + return script + .load() + .then(() => { + expect(script.toString()).eql("{ content: 'var i = 3;var i = 3;var i =...' }"); + }); + }); + }); + + describe('Load', () => { + it('From full path', () => { + const file = createScriptFile(); + const script = new ClientScript({ path: file.name }); + + return script + .load() + .then(() => { + expect(script.content).eql(testScriptContent); + expect(script.path).eql(file.name); + expect(script.url).contains('_'); + expect(script.url).not.contains('/'); + expect(script.url).not.contains('\\'); + expect(script.toString()).eql(`{ path: '${file.name}' }`); + }); + }); + + it('Node module', () => { + const script = new ClientScript({ module: testModuleName }); + + return script.load() + .then(() => { + expect(script.content).contains('hasDockerEnv() || hasDockerCGroup()'); + expect(script.module).eql(testModuleName); + }); + }); + + it('From relative path', () => { + const script = new ClientScript('test/server/data/custom-client-scripts/utils.js', testBasePath); + + return script.load() + .then(() => { + expect(script.content).contains('function test () {'); + }); + }); + }); + + describe('"Page" property', () => { + it('Default value', () => { + const script = new ClientScript({ content: testScriptContent }); + + return script.load() + .then(() => { + const anyRequestFilterRule = RequestFilterRule.ANY; + + expect(script.page.options).eql(anyRequestFilterRule.options); + expect(script.page.id).not.eql(anyRequestFilterRule.id); + }); + }); + + it('Initializer', () => { + const script = new ClientScript( { + page: 'http://example.com', + content: testScriptContent, + }); + + return script.load() + .then(() => { + expect(script.page).to.be.an.instanceOf(RequestFilterRule); + }); + }); + }); + + describe('Should throw the error if "content", "path" and "module" properties were combined', () => { + it('"path" and "content"', () => { + const file = createScriptFile(); + const script = new ClientScript({ path: file.name, content: testScriptContent }); + + return script.load() + .then(() => { + expect.fail('Should throw the error'); + }).catch(e => { + expect(e.message).eql('Client scripts can only have one initializer: JavaScript code, a JavaScript file path, or the name of a JavaScript module.'); + }); + }); + + it('"path" and "module"', () => { + const file = createScriptFile(); + const script = new ClientScript({ path: file.name, module: testModuleName }); + + return script.load() + .then(() => { + expect.fail('Should throw the error'); + }).catch(e => { + expect(e.message).eql('Client scripts can only have one initializer: JavaScript code, a JavaScript file path, or the name of a JavaScript module.'); + }); + }); + + it('"content" and "module"', () => { + const script = new ClientScript({ module: testModuleName, content: testScriptContent }); + + return script.load() + .then(() => { + expect.fail('Should throw the error'); + }).catch(e => { + expect(e.message).eql('Client scripts can only have one initializer: JavaScript code, a JavaScript file path, or the name of a JavaScript module.'); + }); + }); + }); + + it('Should handle IO errors', () => { + return new ClientScript({ path: '/non-existing-file' }, testBasePath) + .load() + .then(() => { + expect.fail('Should throw the error'); + }) + .catch(e => { + expect(e.message).contains( + 'Cannot load a client script from /non-existing-file.\n' + + 'ENOENT: no such file or directory, open'); + }); + }); + + it('Should correct non-unique urls', () => { + const scripts = [ + { module: testModuleName }, + { module: testModuleName }, + ]; + + return loadClientScripts(scripts, testBasePath) + .then(setUniqueUrls) + .then(result => { + expect(result.length).eql(2); + expect(result[0].url).to.not.eql(result[1].url); + }); + }); + + describe('Should provide information about problematic scripts', () => { + describe('Duplicated content', () => { + it('All script applied for all pages', () => { + const scripts = [ + { content: '1' }, + { content: '1' }, + { content: '2' }, + { content: '2' }, + { content: '3' }, + ]; + + return loadClientScripts(scripts) + .then(findProblematicScripts) + .then(({ duplicatedContent }) => { + expect(duplicatedContent.length).eql(2); + expect(duplicatedContent[0].content).eql(scripts[0].content); + expect(duplicatedContent[1].content).eql(scripts[2].content); + }); + }); + + it('At least script applied for all pages', () => { + const scripts = [ + { content: '1' }, + { content: '1', page: 'https://example.com' }, + { content: '2' }, + { content: '2' }, + { content: '3', page: 'https://example.com' }, + { content: '3', page: 'https://example.com' }, + { content: '4', page: 'https://example.com' }, + { content: '4', page: 'https://another-site.com' }, + { content: '5', page: 'https://example.com' }, + ]; + + return loadClientScripts(scripts) + .then(findProblematicScripts) + .then(({ duplicatedContent }) => { + expect(duplicatedContent.length).eql(3); + expect(duplicatedContent[0].content).eql(scripts[0].content); + expect(duplicatedContent[1].content).eql(scripts[2].content); + expect(duplicatedContent[2].content).eql(scripts[4].content); + }); + }); + + it('All scripts applied for the specified pages', () => { + const scripts = [ + { content: '1', page: 'https://example.com' }, + { content: '1', page: 'https://example.com' }, + { content: '2', page: 'https://example.com' }, + { content: '2', page: 'https://another-site.com' }, + { content: '3', page: 'https://example.com' }, + ]; + + return loadClientScripts(scripts) + .then(findProblematicScripts) + .then(({ duplicatedContent }) => { + expect(duplicatedContent.length).eql(1); + expect(duplicatedContent[0].content).eql(scripts[0].content); + }); + }); + }); + + it('Empty content', () => { + const scripts = [ + { content: '' }, + { content: '123' }, + { content: '' }, + ]; + + return loadClientScripts(scripts) + .then(findProblematicScripts) + .then(({ empty }) => { + expect(empty[0].content).is.empty; + expect(empty[1].content).is.empty; + }); + }); + }); + + it('Type assertion', () => { + expect(is.clientScriptInitializer.predicate({})).to.be.false; + expect(is.clientScriptInitializer.predicate(null)).to.be.false; + expect(is.clientScriptInitializer.predicate({ path: '/path' })).to.be.true; + expect(is.clientScriptInitializer.predicate({ content: 'var i = 0' })).to.be.true; + expect(is.clientScriptInitializer.predicate({ module: 'module-name' })).to.be.true; + }); + + it('Get URL', async () => { + const script = new ClientScript({ content: testScriptContent }); + + await script.load(); + + expect(script.getResultUrl('123')).eql('/custom-client-scripts/123/' + script.url); + }); +}); diff --git a/test/server/data/client-fn-compilation/basic/expected-node10.js b/test/server/data/client-fn-compilation/basic/expected-node10.js deleted file mode 100644 index d5f325c71e3..00000000000 --- a/test/server/data/client-fn-compilation/basic/expected-node10.js +++ /dev/null @@ -1,8 +0,0 @@ -(function () { - return (function () { - var _window$location = __get$(window, "location"), - hostname = __get$(_window$location, "hostname"), - port = __get$(_window$location, "port"); - return hostname + ':' + port; - }); -})(); diff --git a/test/server/data/client-fn-compilation/basic/expected.js b/test/server/data/client-fn-compilation/basic/expected.js index d5f325c71e3..f42d54be20d 100644 --- a/test/server/data/client-fn-compilation/basic/expected.js +++ b/test/server/data/client-fn-compilation/basic/expected.js @@ -1,8 +1,9 @@ (function () { - return (function () { + var func = function func () { var _window$location = __get$(window, "location"), - hostname = __get$(_window$location, "hostname"), - port = __get$(_window$location, "port"); + hostname = _window$location.hostname, + port = _window$location.port; return hostname + ':' + port; - }); + }; + return func; })(); diff --git a/test/server/data/client-fn-compilation/gh1279/expected.js b/test/server/data/client-fn-compilation/gh1279/expected.js index 6eacfded871..024dedaf9d2 100644 --- a/test/server/data/client-fn-compilation/gh1279/expected.js +++ b/test/server/data/client-fn-compilation/gh1279/expected.js @@ -1,5 +1,7 @@ (function () { - return function fn_123$$ () { + var func = function fn_123$$ () { return true; }; + + return func; })(); diff --git a/test/server/data/client-fn-compilation/json-stringify/expected.js b/test/server/data/client-fn-compilation/json-stringify/expected.js deleted file mode 100644 index 1cdd90d3952..00000000000 --- a/test/server/data/client-fn-compilation/json-stringify/expected.js +++ /dev/null @@ -1,8 +0,0 @@ -(function () { - var _stringify2 = { default: JSON.stringify }; - return (function () { - var str = (0, _stringify2.default)(someObj); - - return JSON.parse(someStr + str); - }); -})(); diff --git a/test/server/data/client-fn-compilation/json-stringify/testfile.js b/test/server/data/client-fn-compilation/json-stringify/testfile.js deleted file mode 100644 index 3dc8200feab..00000000000 --- a/test/server/data/client-fn-compilation/json-stringify/testfile.js +++ /dev/null @@ -1,15 +0,0 @@ -import compileClientFunction from '../../../../../lib/compiler/compile-client-function'; - -fixture `Fixture`; - -function compile (fn) { - return compileClientFunction(fn.toString()); -} - -test('Test', () => { - return compile(() => { - const str = JSON.stringify(someObj); - - return JSON.parse(someStr + str); - }); -}); diff --git a/test/server/data/client-fn-compilation/object-keys/expected.js b/test/server/data/client-fn-compilation/object-keys/expected.js deleted file mode 100644 index 3b4228cb422..00000000000 --- a/test/server/data/client-fn-compilation/object-keys/expected.js +++ /dev/null @@ -1,8 +0,0 @@ -(function () { - var _keys2 = { default: Object.keys }; - return (function () { - return (0, _keys2.default)(SomeClass.prototype).forEach(function (prop) { - return prop; - }); - }); -})(); diff --git a/test/server/data/client-fn-compilation/object-keys/testfile.js b/test/server/data/client-fn-compilation/object-keys/testfile.js deleted file mode 100644 index 35060251e18..00000000000 --- a/test/server/data/client-fn-compilation/object-keys/testfile.js +++ /dev/null @@ -1,13 +0,0 @@ -import compileClientFunction from '../../../../../lib/compiler/compile-client-function'; - -fixture `Fixture`; - -function compile (fn) { - return compileClientFunction(fn.toString()); -} - -test('Test', () => { - return compile(() => { - return Object.keys(SomeClass.prototype).forEach(prop => prop); - }); -}); diff --git a/test/server/data/client-fn-compilation/performance/index.js b/test/server/data/client-fn-compilation/performance/index.js new file mode 100644 index 00000000000..1b8e71328c9 --- /dev/null +++ b/test/server/data/client-fn-compilation/performance/index.js @@ -0,0 +1,13 @@ +import { Selector } from 'testcafe'; + +const selectors = []; + +for (let i = 0; i < 300; i++) { + const str = i.toString(); + + selectors.push(Selector(str).withAttribute(str).find(str)); +} + +fixture ('Fixture'); + +test('test', async t => { }); diff --git a/test/server/data/client-fn-compilation/promises/expected.js b/test/server/data/client-fn-compilation/promises/expected.js deleted file mode 100644 index 2b6ae2fa576..00000000000 --- a/test/server/data/client-fn-compilation/promises/expected.js +++ /dev/null @@ -1,15 +0,0 @@ -(function () { - var _promise2 = { default: Promise }; - return (function () { - var a = new _promise2.default(function (resolve, reject) { - reject(1); - }); - - - return _promise2.default.resolve().then(function () { - return a; - }).catch(function (err) { - return err; - }); - }); -})(); diff --git a/test/server/data/client-fn-compilation/promises/testfile.js b/test/server/data/client-fn-compilation/promises/testfile.js deleted file mode 100644 index 15f5af4bcee..00000000000 --- a/test/server/data/client-fn-compilation/promises/testfile.js +++ /dev/null @@ -1,20 +0,0 @@ -import compileClientFunction from '../../../../../lib/compiler/compile-client-function'; - -fixture `Fixture`; - -function compile (fn) { - return compileClientFunction(fn.toString()); -} - -test('Test', () => { - return compile(() => { - var a = new Promise((resolve, reject) => { - reject(1); - }); - - return Promise - .resolve() - .then(()=> a) - .catch(err => err); - }); -}); diff --git a/test/server/data/client-fn-compilation/typeof/expected-node10.js b/test/server/data/client-fn-compilation/typeof/expected-node10.js index 184641e3258..36337b2a2c9 100644 --- a/test/server/data/client-fn-compilation/typeof/expected-node10.js +++ b/test/server/data/client-fn-compilation/typeof/expected-node10.js @@ -4,7 +4,9 @@ return typeof obj; } }; - return (function () { + var func = (function () { return typeof someObj === "undefined" ? "undefined" : (0, _typeof3.default)(someObj); }); + + return func; })(); diff --git a/test/server/data/client-fn-compilation/typeof/expected.js b/test/server/data/client-fn-compilation/typeof/expected.js index 6a23f70b0c9..6bd66831745 100644 --- a/test/server/data/client-fn-compilation/typeof/expected.js +++ b/test/server/data/client-fn-compilation/typeof/expected.js @@ -2,7 +2,8 @@ var _typeof = function (obj) { return typeof obj; }; - return (function () { + var func = (function () { return typeof someObj === "undefined" ? "undefined" : _typeof(someObj); }); + return func; })(); diff --git a/test/server/data/configuration/module-not-found/config.js b/test/server/data/configuration/module-not-found/config.js new file mode 100644 index 00000000000..8ace7f3955f --- /dev/null +++ b/test/server/data/configuration/module-not-found/config.js @@ -0,0 +1,5 @@ +const existingModule = require('./module.js'); + +module.exports = { + reporters: [existingModule] +} diff --git a/test/server/data/configuration/module-not-found/module.js b/test/server/data/configuration/module-not-found/module.js new file mode 100644 index 00000000000..7c0557434a8 --- /dev/null +++ b/test/server/data/configuration/module-not-found/module.js @@ -0,0 +1,3 @@ +const nonExist = require('non-existing-module'); + +module.exports = {} diff --git a/test/server/data/custom-actions/config.js b/test/server/data/custom-actions/config.js new file mode 100644 index 00000000000..d29f9a21165 --- /dev/null +++ b/test/server/data/custom-actions/config.js @@ -0,0 +1,11 @@ +module.exports = { + customActions: { + async makeSomething () { + await this.click(); + }, + async doSomething () { + await this.custom.makeSomething(); + }, + fake: 'some fake data' + } +} diff --git a/test/server/data/custom-client-scripts/utils.js b/test/server/data/custom-client-scripts/utils.js new file mode 100644 index 00000000000..b82094495e2 --- /dev/null +++ b/test/server/data/custom-client-scripts/utils.js @@ -0,0 +1,3 @@ +function test () { + +} diff --git a/test/server/data/execute-async-expression/runtime-error b/test/server/data/execute-async-expression/runtime-error new file mode 100644 index 00000000000..3538d93459c --- /dev/null +++ b/test/server/data/execute-async-expression/runtime-error @@ -0,0 +1,12 @@ +An unhandled error occurred in the custom script: + +Cannot prepare tests due to the following error: + +Cannot initialize a Selector because Selector is undefined, and not one of the following: a CSS selector string, a Selector object, a node snapshot, a function, or a Promise returned by a Selector. + +const s = Selector(); + +Browser: Chrome 15.0.874.120 / macOS 10.15 +Screenshot: /unix/path/with/ + +1 diff --git a/test/server/data/execute-async-expression/test-run-error b/test/server/data/execute-async-expression/test-run-error new file mode 100644 index 00000000000..73cc3120372 --- /dev/null +++ b/test/server/data/execute-async-expression/test-run-error @@ -0,0 +1,11 @@ +An unhandled error occurred in the custom script: + +The "timeout" argument is expected to be a positive integer, but it was string. + +Browser: Chrome 15.0.874.120 / macOS 10.15 + + > 1 |await t.wait('10'); + +Screenshot: /unix/path/with/ + +1 diff --git a/test/server/data/execution-context/node_modules/module-for-test/index.js b/test/server/data/execution-context/node_modules/module-for-test/index.js new file mode 100644 index 00000000000..03534135d8b --- /dev/null +++ b/test/server/data/execution-context/node_modules/module-for-test/index.js @@ -0,0 +1,5 @@ +function moduleLoaded () { + return true; +} + +module.exports = moduleLoaded; \ No newline at end of file diff --git a/test/server/data/expected-legacy-test-run-errors/empty-first-argument b/test/server/data/expected-legacy-test-run-errors/empty-first-argument index 1d91615b3a6..137dbaa45f0 100644 --- a/test/server/data/expected-legacy-test-run-errors/empty-first-argument +++ b/test/server/data/expected-legacy-test-run-errors/empty-first-argument @@ -1,10 +1,8 @@ Error at step "Step with ": A target element of the testAction action has not been found in the DOM tree. -If this element should be created after animation or a time-consuming -operation is finished, use the waitFor action (available for use in code) to -pause test execution until this element appears. +If this element should be created after animation or a time-consuming operation is finished, use the waitFor action (available for use in code) to pause test execution until this element appears. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/empty-iframe-argument b/test/server/data/expected-legacy-test-run-errors/empty-iframe-argument index 30436342abf..b59f3dd0a0d 100644 --- a/test/server/data/expected-legacy-test-run-errors/empty-iframe-argument +++ b/test/server/data/expected-legacy-test-run-errors/empty-iframe-argument @@ -1,5 +1,5 @@ Error at step "Step with ": The selector within the inIFrame function returns an empty value. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/empty-type-action-argument b/test/server/data/expected-legacy-test-run-errors/empty-type-action-argument index 6ece757187c..0b3801cd696 100644 --- a/test/server/data/expected-legacy-test-run-errors/empty-type-action-argument +++ b/test/server/data/expected-legacy-test-run-errors/empty-type-action-argument @@ -1,7 +1,7 @@ Error at step "Step with ": The type action's parameter text is empty. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/eq-assertion b/test/server/data/expected-legacy-test-run-errors/eq-assertion index 1c3509debf5..630b2e410c8 100644 --- a/test/server/data/expected-legacy-test-run-errors/eq-assertion +++ b/test/server/data/expected-legacy-test-run-errors/eq-assertion @@ -6,7 +6,7 @@ Expected: "" Actual: "" ^ -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/expected-dialog-doesnt-appear b/test/server/data/expected-legacy-test-run-errors/expected-dialog-doesnt-appear index 9ce6a9166e6..e184eb59b42 100644 --- a/test/server/data/expected-legacy-test-run-errors/expected-dialog-doesnt-appear +++ b/test/server/data/expected-legacy-test-run-errors/expected-dialog-doesnt-appear @@ -1,5 +1,5 @@ Error at step "Step with ": The expected system test-dialog dialog did not appear. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/global-wait-for-action-timeout-exceed b/test/server/data/expected-legacy-test-run-errors/global-wait-for-action-timeout-exceed index 4e0e9b46645..9f76aa1fd1f 100644 --- a/test/server/data/expected-legacy-test-run-errors/global-wait-for-action-timeout-exceed +++ b/test/server/data/expected-legacy-test-run-errors/global-wait-for-action-timeout-exceed @@ -1,5 +1,5 @@ Error at step "Step with ": __waitFor action's timeout exceeded. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/iframe-argument-is-not-iframe b/test/server/data/expected-legacy-test-run-errors/iframe-argument-is-not-iframe index d1ed005648e..b90e8bd32a5 100644 --- a/test/server/data/expected-legacy-test-run-errors/iframe-argument-is-not-iframe +++ b/test/server/data/expected-legacy-test-run-errors/iframe-argument-is-not-iframe @@ -1,5 +1,5 @@ Error at step "Step with ": The selector within the inIFrame function doesn’t return an iframe element. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/iframe-loading-timeout b/test/server/data/expected-legacy-test-run-errors/iframe-loading-timeout index 9f23cd7bcc0..5c946323039 100644 --- a/test/server/data/expected-legacy-test-run-errors/iframe-loading-timeout +++ b/test/server/data/expected-legacy-test-run-errors/iframe-loading-timeout @@ -1,4 +1,4 @@ IFrame loading timed out. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/in-iframe-target-loading-timeout b/test/server/data/expected-legacy-test-run-errors/in-iframe-target-loading-timeout index 464c950e4db..b4e64620ce9 100644 --- a/test/server/data/expected-legacy-test-run-errors/in-iframe-target-loading-timeout +++ b/test/server/data/expected-legacy-test-run-errors/in-iframe-target-loading-timeout @@ -1,5 +1,5 @@ Error at step "Step with ": IFrame target loading timed out. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-dragging-second-argument b/test/server/data/expected-legacy-test-run-errors/incorrect-dragging-second-argument index 75f8b4c42cd..3ba505c9f95 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-dragging-second-argument +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-dragging-second-argument @@ -1,7 +1,7 @@ Error at step "Step with ": drag action drop target is incorrect. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-event-argument b/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-event-argument index 1e62421a5e3..1b1314a09c5 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-event-argument +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-event-argument @@ -1,5 +1,5 @@ Error at step "Step with ": __waitFor action's first parameter should be a function. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-timeout-argument b/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-timeout-argument index 6d648782469..db325e32ebe 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-timeout-argument +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-global-wait-for-action-timeout-argument @@ -1,5 +1,5 @@ Error at step "Step with ": __waitFor action's "timeout" parameter should be a positive number. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-iframe-argument b/test/server/data/expected-legacy-test-run-errors/incorrect-iframe-argument index 98c3bb746a4..77f52b8346d 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-iframe-argument +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-iframe-argument @@ -1,5 +1,5 @@ Error at step "Step with ": The inIFrame function contains an invalid argument. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-press-action-argument b/test/server/data/expected-legacy-test-run-errors/incorrect-press-action-argument index dcf045bbdd7..0375c03ee64 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-press-action-argument +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-press-action-argument @@ -1,7 +1,7 @@ Error at step "Step with ": press action parameter contains incorrect key code. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-select-action-arguments b/test/server/data/expected-legacy-test-run-errors/incorrect-select-action-arguments index a7463eaa337..857d07018be 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-select-action-arguments +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-select-action-arguments @@ -1,7 +1,7 @@ Error at step "Step with ": select action's parameters contain an incorrect value. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-wait-action-milliseconds-arguments b/test/server/data/expected-legacy-test-run-errors/incorrect-wait-action-milliseconds-arguments index ff853dc86c3..3313acd79ca 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-wait-action-milliseconds-arguments +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-wait-action-milliseconds-arguments @@ -1,7 +1,7 @@ Error at step "Step with ": wait action's "milliseconds" parameter should be a positive number. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-event-argument b/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-event-argument index e027b77e8c9..bcf4e335952 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-event-argument +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-event-argument @@ -1,8 +1,7 @@ Error at step "Step with ": -waitFor action's first parameter should be a function, a CSS selector or an -array of CSS selectors. +waitFor action's first parameter should be a function, a CSS selector or an array of CSS selectors. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-timeout-argument b/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-timeout-argument index 03416fb02ad..093ec45bf64 100644 --- a/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-timeout-argument +++ b/test/server/data/expected-legacy-test-run-errors/incorrect-wait-for-action-timeout-argument @@ -1,7 +1,7 @@ Error at step "Step with ": waitFor action's "timeout" parameter should be a positive number. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/invisible-action-element b/test/server/data/expected-legacy-test-run-errors/invisible-action-element index 83ce7ce8b87..d509950a330 100644 --- a/test/server/data/expected-legacy-test-run-errors/invisible-action-element +++ b/test/server/data/expected-legacy-test-run-errors/invisible-action-element @@ -1,9 +1,8 @@ Error at step "Step with ": A target element of the test-action action is not visible. -If this element should appear when you are hovering over another element, -make sure that you properly recorded the hover action. +If this element should appear when you are hovering over another element, make sure that you properly recorded the hover action. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/multiple-iframe-argument b/test/server/data/expected-legacy-test-run-errors/multiple-iframe-argument index 3f1bbdc9c1b..f4eaec7bdf4 100644 --- a/test/server/data/expected-legacy-test-run-errors/multiple-iframe-argument +++ b/test/server/data/expected-legacy-test-run-errors/multiple-iframe-argument @@ -1,6 +1,5 @@ Error at step "Step with ": -The selector within the inIFrame function returns more than one iframe -element. +The selector within the inIFrame function returns more than one iframe element. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/not-eq-assertion b/test/server/data/expected-legacy-test-run-errors/not-eq-assertion index ce9c0597019..47d23c6bb53 100644 --- a/test/server/data/expected-legacy-test-run-errors/not-eq-assertion +++ b/test/server/data/expected-legacy-test-run-errors/not-eq-assertion @@ -3,7 +3,7 @@ Expected: not "" Actual: "" -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/not-ok-assertion b/test/server/data/expected-legacy-test-run-errors/not-ok-assertion index 198b857f10a..ca9fe5e3c36 100644 --- a/test/server/data/expected-legacy-test-run-errors/not-ok-assertion +++ b/test/server/data/expected-legacy-test-run-errors/not-ok-assertion @@ -3,7 +3,7 @@ Expected: null, undefined, false, NaN or '' Actual: "" -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/ok-assertion b/test/server/data/expected-legacy-test-run-errors/ok-assertion index 8ac9a55441d..fe19abae144 100644 --- a/test/server/data/expected-legacy-test-run-errors/ok-assertion +++ b/test/server/data/expected-legacy-test-run-errors/ok-assertion @@ -3,7 +3,7 @@ Expected: not null, not undefined, not false, not NaN and not '' Actual: false -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/page-not-loaded b/test/server/data/expected-legacy-test-run-errors/page-not-loaded index 6b31afc5c62..b0e0030de6c 100644 --- a/test/server/data/expected-legacy-test-run-errors/page-not-loaded +++ b/test/server/data/expected-legacy-test-run-errors/page-not-loaded @@ -1,4 +1,4 @@ Failed to find a DNS-record for the resource at "example.org". -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/store-dom-node-or-jquery-object b/test/server/data/expected-legacy-test-run-errors/store-dom-node-or-jquery-object index 5e4f5508bd6..feb17059878 100644 --- a/test/server/data/expected-legacy-test-run-errors/store-dom-node-or-jquery-object +++ b/test/server/data/expected-legacy-test-run-errors/store-dom-node-or-jquery-object @@ -1,6 +1,5 @@ Error at step "Step with ": -It is not allowed to share the DOM element, jQuery object or a function -between test steps via "this" object. +It is not allowed to share the DOM element, jQuery object or a function between test steps via "this" object. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/uncaught-js-error b/test/server/data/expected-legacy-test-run-errors/uncaught-js-error index cf1bc02215a..5785f90c972 100644 --- a/test/server/data/expected-legacy-test-run-errors/uncaught-js-error +++ b/test/server/data/expected-legacy-test-run-errors/uncaught-js-error @@ -1,4 +1,4 @@ Uncaught JavaScript error test-error-with- on page "http://page" -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/uncaught-js-error-in-test-code-step b/test/server/data/expected-legacy-test-run-errors/uncaught-js-error-in-test-code-step index 7810bdf70be..61d164a1a14 100644 --- a/test/server/data/expected-legacy-test-run-errors/uncaught-js-error-in-test-code-step +++ b/test/server/data/expected-legacy-test-run-errors/uncaught-js-error-in-test-code-step @@ -1,5 +1,5 @@ Error at step "Step with ": Uncaught JavaScript error in test code - error with . -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/unexpected-dialog b/test/server/data/expected-legacy-test-run-errors/unexpected-dialog index 80c300c1a56..dfcf5ecbe9a 100644 --- a/test/server/data/expected-legacy-test-run-errors/unexpected-dialog +++ b/test/server/data/expected-legacy-test-run-errors/unexpected-dialog @@ -1,5 +1,5 @@ Error at step "Step with ": Unexpected system test-dialog dialog message with appeared. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ diff --git a/test/server/data/expected-legacy-test-run-errors/upload-can-not-find-file-to-upload b/test/server/data/expected-legacy-test-run-errors/upload-can-not-find-file-to-upload index 7d4dbe9f13a..2b58a486148 100644 --- a/test/server/data/expected-legacy-test-run-errors/upload-can-not-find-file-to-upload +++ b/test/server/data/expected-legacy-test-run-errors/upload-can-not-find-file-to-upload @@ -4,7 +4,7 @@ Cannot find the following file(s) to upload: /unix/path/with/, path2 -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/upload-element-is-not-file-input b/test/server/data/expected-legacy-test-run-errors/upload-element-is-not-file-input index 0b0b59eb4b3..6cdcc5f6dab 100644 --- a/test/server/data/expected-legacy-test-run-errors/upload-element-is-not-file-input +++ b/test/server/data/expected-legacy-test-run-errors/upload-element-is-not-file-input @@ -1,7 +1,7 @@ Error at step "Step with ": upload action argument does not contain a file input element. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/upload-invalid-file-path-argument b/test/server/data/expected-legacy-test-run-errors/upload-invalid-file-path-argument index 8af1727b3be..3e1368b6ec6 100644 --- a/test/server/data/expected-legacy-test-run-errors/upload-invalid-file-path-argument +++ b/test/server/data/expected-legacy-test-run-errors/upload-invalid-file-path-argument @@ -1,7 +1,7 @@ Error at step "Step with ": upload action's "path" parameter should be a string or an array of strings. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-legacy-test-run-errors/wait-for-action-timeout-exceeded b/test/server/data/expected-legacy-test-run-errors/wait-for-action-timeout-exceeded index 5b66aea4a72..f7abe9bf4de 100644 --- a/test/server/data/expected-legacy-test-run-errors/wait-for-action-timeout-exceeded +++ b/test/server/data/expected-legacy-test-run-errors/wait-for-action-timeout-exceeded @@ -1,7 +1,7 @@ Error at step "Step with ": waitFor action's timeout exceeded. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ Code: diff --git a/test/server/data/expected-test-run-errors/action-additional-element-is-invisible-error b/test/server/data/expected-test-run-errors/action-additional-element-is-invisible-error index e90f18208d2..bbd3ba8ab7a 100644 --- a/test/server/data/expected-test-run-errors/action-additional-element-is-invisible-error +++ b/test/server/data/expected-test-run-errors/action-additional-element-is-invisible-error @@ -1,6 +1,6 @@ The element that matches the specified "startSelector" is not visible. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-additional-element-not-found-error b/test/server/data/expected-test-run-errors/action-additional-element-not-found-error index 18a1fa37d6e..21c48359f43 100644 --- a/test/server/data/expected-test-run-errors/action-additional-element-not-found-error +++ b/test/server/data/expected-test-run-errors/action-additional-element-not-found-error @@ -1,6 +1,11 @@ The specified "startSelector" does not match any element in the DOM tree. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +  | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua + > | one +  | two +  | three + +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-additional-selector-matches-wrong-node-type-error b/test/server/data/expected-test-run-errors/action-additional-selector-matches-wrong-node-type-error index 0739fe72928..f8de313003a 100644 --- a/test/server/data/expected-test-run-errors/action-additional-selector-matches-wrong-node-type-error +++ b/test/server/data/expected-test-run-errors/action-additional-selector-matches-wrong-node-type-error @@ -1,7 +1,6 @@ -The specified "startSelector" is expected to match a DOM element, but it -matches a text node. +The specified "startSelector" is expected to match a DOM element, but it matches a text node. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-boolean-argument-error b/test/server/data/expected-test-run-errors/action-boolean-argument-error new file mode 100644 index 00000000000..b332db624ae --- /dev/null +++ b/test/server/data/expected-test-run-errors/action-boolean-argument-error @@ -0,0 +1,19 @@ +The "isAsyncExpression" argument is expected to be a boolean value, but it was object. + +Browser: Chrome 15.0.874.120 / macOS 10.15 +Screenshot: /unix/path/with/ + + 18 |function func1 () { + 19 | record = createCallsiteRecord({ byFunctionName: 'func1' }); + 20 |} + 21 | + 22 |(function func2 () { + > 23 | func1(); + 24 |})(); + 25 | + 26 |stackTrace.filter.deattach(stackFilter); + 27 | + 28 |module.exports = record; + + at func2 (testfile.js:23:5) + at Object. (testfile.js:24:3) diff --git a/test/server/data/expected-test-run-errors/action-boolean-option-error b/test/server/data/expected-test-run-errors/action-boolean-option-error index 725b9f85371..1860a952cec 100644 --- a/test/server/data/expected-test-run-errors/action-boolean-option-error +++ b/test/server/data/expected-test-run-errors/action-boolean-option-error @@ -1,7 +1,6 @@ -The "modifier.ctrl" option is expected to be a boolean value, but it was -object. +The "modifier.ctrl" option is expected to be a boolean value, but it was object. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-can-not-find-file-to-upload-error b/test/server/data/expected-test-run-errors/action-can-not-find-file-to-upload-error deleted file mode 100644 index ce387e43b29..00000000000 --- a/test/server/data/expected-test-run-errors/action-can-not-find-file-to-upload-error +++ /dev/null @@ -1,21 +0,0 @@ -Cannot find the following file(s) to upload: -/path/1 -/path/2 - -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 -Screenshot: /unix/path/with/ - - 18 |function func1 () { - 19 | record = createCallsiteRecord({ byFunctionName: 'func1' }); - 20 |} - 21 | - 22 |(function func2 () { - > 23 | func1(); - 24 |})(); - 25 | - 26 |stackTrace.filter.deattach(stackFilter); - 27 | - 28 |module.exports = record; - - at func2 (testfile.js:23:5) - at Object. (testfile.js:24:3) diff --git a/test/server/data/expected-test-run-errors/action-cannot-find-file-to-upload-error b/test/server/data/expected-test-run-errors/action-cannot-find-file-to-upload-error new file mode 100644 index 00000000000..fd5ff386e88 --- /dev/null +++ b/test/server/data/expected-test-run-errors/action-cannot-find-file-to-upload-error @@ -0,0 +1,27 @@ +Cannot find the following file(s) to upload: +/path/1 +/path/2 + +The following locations were scanned for the missing upload files: +full-path-to/path/1 +full-path-to/path/2 + +Ensure these files exist or change the working directory. + +Browser: Chrome 15.0.874.120 / macOS 10.15 +Screenshot: /unix/path/with/ + + 18 |function func1 () { + 19 | record = createCallsiteRecord({ byFunctionName: 'func1' }); + 20 |} + 21 | + 22 |(function func2 () { + > 23 | func1(); + 24 |})(); + 25 | + 26 |stackTrace.filter.deattach(stackFilter); + 27 | + 28 |module.exports = record; + + at func2 (testfile.js:23:5) + at Object. (testfile.js:24:3) diff --git a/test/server/data/expected-test-run-errors/action-cookie-argument-error b/test/server/data/expected-test-run-errors/action-cookie-argument-error new file mode 100644 index 00000000000..7f13195943d --- /dev/null +++ b/test/server/data/expected-test-run-errors/action-cookie-argument-error @@ -0,0 +1,19 @@ +The value of the "cookies" argument does not belong to an acceptable data type: Object, String, or Array of objects or strings. + +Browser: Chrome 15.0.874.120 / macOS 10.15 +Screenshot: /unix/path/with/ + + 18 |function func1 () { + 19 | record = createCallsiteRecord({ byFunctionName: 'func1' }); + 20 |} + 21 | + 22 |(function func2 () { + > 23 | func1(); + 24 |})(); + 25 | + 26 |stackTrace.filter.deattach(stackFilter); + 27 | + 28 |module.exports = record; + + at func2 (testfile.js:23:5) + at Object. (testfile.js:24:3) diff --git a/test/server/data/expected-test-run-errors/action-cookie-arguments-error b/test/server/data/expected-test-run-errors/action-cookie-arguments-error new file mode 100644 index 00000000000..e307e60feff --- /dev/null +++ b/test/server/data/expected-test-run-errors/action-cookie-arguments-error @@ -0,0 +1,19 @@ +The value of cookie number 1 (false) does not belong to an acceptable data type: Object or String. + +Browser: Chrome 15.0.874.120 / macOS 10.15 +Screenshot: /unix/path/with/ + + 18 |function func1 () { + 19 | record = createCallsiteRecord({ byFunctionName: 'func1' }); + 20 |} + 21 | + 22 |(function func2 () { + > 23 | func1(); + 24 |})(); + 25 | + 26 |stackTrace.filter.deattach(stackFilter); + 27 | + 28 |module.exports = record; + + at func2 (testfile.js:23:5) + at Object. (testfile.js:24:3) diff --git a/test/server/data/expected-test-run-errors/action-date-option-error b/test/server/data/expected-test-run-errors/action-date-option-error new file mode 100644 index 00000000000..914ba374045 --- /dev/null +++ b/test/server/data/expected-test-run-errors/action-date-option-error @@ -0,0 +1,19 @@ +The value of the "expires" option belongs to an unsupported data type (string). The "expires" option only accepts Date type values. + +Browser: Chrome 15.0.874.120 / macOS 10.15 +Screenshot: /unix/path/with/ + + 18 |function func1 () { + 19 | record = createCallsiteRecord({ byFunctionName: 'func1' }); + 20 |} + 21 | + 22 |(function func2 () { + > 23 | func1(); + 24 |})(); + 25 | + 26 |stackTrace.filter.deattach(stackFilter); + 27 | + 28 |module.exports = record; + + at func2 (testfile.js:23:5) + at Object. (testfile.js:24:3) diff --git a/test/server/data/expected-test-run-errors/action-element-is-invisible-error b/test/server/data/expected-test-run-errors/action-element-is-invisible-error index 1764c6968e4..604f87c3b86 100644 --- a/test/server/data/expected-test-run-errors/action-element-is-invisible-error +++ b/test/server/data/expected-test-run-errors/action-element-is-invisible-error @@ -1,6 +1,6 @@ The element that matches the specified selector is not visible. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-element-is-not-file-input-error b/test/server/data/expected-test-run-errors/action-element-is-not-file-input-error index b65b297a46f..38048107dcb 100644 --- a/test/server/data/expected-test-run-errors/action-element-is-not-file-input-error +++ b/test/server/data/expected-test-run-errors/action-element-is-not-file-input-error @@ -1,6 +1,6 @@ The specified selector does not match a file input element. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-element-is-not-target-error b/test/server/data/expected-test-run-errors/action-element-is-not-target-error new file mode 100644 index 00000000000..9d0de4cd7a9 --- /dev/null +++ b/test/server/data/expected-test-run-errors/action-element-is-not-target-error @@ -0,0 +1,19 @@ +The element that matches the specified selector is covered. + +Browser: Chrome 15.0.874.120 / macOS 10.15 +Screenshot: /unix/path/with/ + + 18 |function func1 () { + 19 | record = createCallsiteRecord({ byFunctionName: 'func1' }); + 20 |} + 21 | + 22 |(function func2 () { + > 23 | func1(); + 24 |})(); + 25 | + 26 |stackTrace.filter.deattach(stackFilter); + 27 | + 28 |module.exports = record; + + at func2 (testfile.js:23:5) + at Object. (testfile.js:24:3) diff --git a/test/server/data/expected-test-run-errors/action-element-non-content-editable-error b/test/server/data/expected-test-run-errors/action-element-non-content-editable-error index 3a77e2c79d8..c4a4107c6b3 100644 --- a/test/server/data/expected-test-run-errors/action-element-non-content-editable-error +++ b/test/server/data/expected-test-run-errors/action-element-non-content-editable-error @@ -1,8 +1,6 @@ -The element that matches the specified "startSelector" is expected to have -the contentEditable attribute enabled or the entire document should be in -design mode. +The element that matches the specified "startSelector" is expected to have the contentEditable attribute enabled or the entire document should be in design mode. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-element-non-editable-error b/test/server/data/expected-test-run-errors/action-element-non-editable-error index 1951ea59b71..e085d4328bb 100644 --- a/test/server/data/expected-test-run-errors/action-element-non-editable-error +++ b/test/server/data/expected-test-run-errors/action-element-non-editable-error @@ -1,7 +1,6 @@ -The action element is expected to be editable (an input, textarea or element -with the contentEditable attribute). +The action element is expected to be editable (an input, textarea or element with the contentEditable attribute). -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-element-not-found-error b/test/server/data/expected-test-run-errors/action-element-not-found-error index 9d9ef542e43..678220b858e 100644 --- a/test/server/data/expected-test-run-errors/action-element-not-found-error +++ b/test/server/data/expected-test-run-errors/action-element-not-found-error @@ -1,6 +1,11 @@ The specified selector does not match any element in the DOM tree. -Browser: Chrome 15.0.874 / Mac OS X 10.8.1 +  | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua + > | one +  | two +  | three + +Browser: Chrome 15.0.874.120 / macOS 10.15 Screenshot: /unix/path/with/ 18 |function func1 () { diff --git a/test/server/data/expected-test-run-errors/action-element-not-iframe-error b/test/server/data/expected-test-run-errors/action-element-not-iframe-error index dc6713243d6..73409343588 100644 --- a/test/server/data/expected-test-run-errors/action-element-not-iframe-error +++ b/test/server/data/expected-test-run-errors/action-element-not-iframe-error @@ -1,6 +1,6 @@ The action element is expected to be an