diff --git a/.gitattributes b/.gitattributes index 1a17ffe37..aeb6ff0bf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ src/locales/** linguist-generated src/types/* text eol=lf +* text=auto eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2587c04a7..2ab6674d4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,36 +1,36 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" - target-branch: "main" - open-pull-requests-limit: 20 - labels: - - "dependencies" - groups: - eslint: - patterns: - - "eslint*" - - "@typescript-eslint*" - stylelint: - patterns: - - "stylelint*" - typescript: - patterns: - - "typedoc" - - "typescript" - - "@types/*" - - "@typescript-eslint*" - rollup: - patterns: - - "@rollup/*" - - "rollup-*" - - "rollup" - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - target-branch: "main" - labels: - - "dependencies" + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'daily' + target-branch: 'main' + open-pull-requests-limit: 20 + labels: + - 'dependencies' + groups: + eslint: + patterns: + - 'eslint*' + - '@typescript-eslint*' + stylelint: + patterns: + - 'stylelint*' + typescript: + patterns: + - 'typedoc' + - 'typescript' + - '@types/*' + - '@typescript-eslint*' + rollup: + patterns: + - '@rollup/*' + - 'rollup-*' + - 'rollup' + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' + target-branch: 'main' + labels: + - 'dependencies' diff --git a/.github/scripts/diff-directories.js b/.github/scripts/diff-directories.js index 465a0afc4..2f1219b17 100644 --- a/.github/scripts/diff-directories.js +++ b/.github/scripts/diff-directories.js @@ -1,124 +1,124 @@ -import fs from 'fs' -import path from 'path' +import fs from 'fs'; +import path from 'path'; -function readFilesRecursively (directory) { - const filenames = fs.readdirSync(directory) - const files = {} +function readFilesRecursively(directory) { + const filenames = fs.readdirSync(directory); + const files = {}; filenames.forEach((filename) => { - const filePath = path.join(directory, filename) - const fileStats = fs.statSync(filePath) + const filePath = path.join(directory, filename); + const fileStats = fs.statSync(filePath); if (fileStats.isDirectory()) { - const nestedFiles = readFilesRecursively(filePath) + const nestedFiles = readFilesRecursively(filePath); for (const [nestedFilePath, nestedFileContent] of Object.entries(nestedFiles)) { - files[path.join(filename, nestedFilePath)] = nestedFileContent + files[path.join(filename, nestedFilePath)] = nestedFileContent; } } else { - files[filename] = fs.readFileSync(filePath, 'utf-8') + files[filename] = fs.readFileSync(filePath, 'utf-8'); } - }) + }); - return files + return files; } -function upperCaseFirstLetter (string) { - return string.charAt(0).toUpperCase() + string.slice(1) +function upperCaseFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); } -function displayDiffs (dir1Files, dir2Files, isOpen) { - const rollupGrouping = {} +function displayDiffs(dir1Files, dir2Files, isOpen) { + const rollupGrouping = {}; /** * Rolls up multiple files with the same diff into a single entry - * @param {string} fileName - * @param {string} string + * @param {string} fileName + * @param {string} string * @param {string} [summary] */ function add(fileName, string, summary = undefined) { if (summary === undefined) { - summary = string + summary = string; } if (!(summary in rollupGrouping)) { - rollupGrouping[summary] = { files: [] } + rollupGrouping[summary] = { files: [] }; } - rollupGrouping[summary].files.push(fileName) - rollupGrouping[summary].string = string + rollupGrouping[summary].files.push(fileName); + rollupGrouping[summary].string = string; } for (const [filePath, fileContent] of Object.entries(dir1Files)) { - let diffOut = '' - let compareOut + let diffOut = ''; + let compareOut; if (filePath in dir2Files) { - const fileOut = fileContent - const file2Out = dir2Files[filePath] - delete dir2Files[filePath] + const fileOut = fileContent; + const file2Out = dir2Files[filePath]; + delete dir2Files[filePath]; if (fileOut === file2Out) { - continue + continue; } else { - compareOut = filePath.split('/')[0] - diffOut = `File has changed` + compareOut = filePath.split('/')[0]; + diffOut = `File has changed`; } } else { - diffOut = '❌ File only exists in old changeset' - compareOut = 'Removed Files' + diffOut = '❌ File only exists in old changeset'; + compareOut = 'Removed Files'; } - add(filePath, diffOut, compareOut) + add(filePath, diffOut, compareOut); } for (const filePath of Object.keys(dir2Files)) { - add(filePath, '❌ File only exists in new changeset', 'New Files') + add(filePath, '❌ File only exists in new changeset', 'New Files'); } - const outString = Object.keys(rollupGrouping).map(key => { - const rollup = rollupGrouping[key] - let outString = ` - ` - const title = key - if (rollup.files.length) { - for (const file of rollup.files) { - outString += `- ${file}\n` + const outString = Object.keys(rollupGrouping) + .map((key) => { + const rollup = rollupGrouping[key]; + let outString = ` + `; + const title = key; + if (rollup.files.length) { + for (const file of rollup.files) { + outString += `- ${file}\n`; + } } - } - outString += '\n\n' + rollup.string - return renderDetails(title, outString, isOpen) - }).join('\n') - return outString + outString += '\n\n' + rollup.string; + return renderDetails(title, outString, isOpen); + }) + .join('\n'); + return outString; } -function renderDetails (section, text, isOpen) { +function renderDetails(section, text, isOpen) { if (section === 'dist') { - section = 'apple' + section = 'apple'; } - const open = section !== 'integration' ? 'open' : '' + const open = section !== 'integration' ? 'open' : ''; return `
${upperCaseFirstLetter(section)} ${text} -
` +`; } if (process.argv.length !== 4) { - console.error('Usage: node diff_directories.js ') - process.exit(1) + console.error('Usage: node diff_directories.js '); + process.exit(1); } -const dir1 = process.argv[2] -const dir2 = process.argv[3] +const dir1 = process.argv[2]; +const dir2 = process.argv[3]; -const sections = { -} -function sortFiles (dirFiles, dirName) { +const sections = {}; +function sortFiles(dirFiles, dirName) { for (const [filePath, fileContent] of Object.entries(dirFiles)) { - sections[dirName] = sections[dirName] || {} - sections[dirName][filePath] = fileContent + sections[dirName] = sections[dirName] || {}; + sections[dirName][filePath] = fileContent; } } -const buildDir = '/build' -const sourcesOutput = '/Sources/ContentScopeScripts/' -sortFiles(readFilesRecursively(dir1 + buildDir), 'dir1') -sortFiles(readFilesRecursively(dir2 + buildDir), 'dir2') -sortFiles(readFilesRecursively(dir1 + sourcesOutput), 'dir1') -sortFiles(readFilesRecursively(dir2 + sourcesOutput), 'dir2') - +const buildDir = '/build'; +const sourcesOutput = '/Sources/ContentScopeScripts/'; +sortFiles(readFilesRecursively(dir1 + buildDir), 'dir1'); +sortFiles(readFilesRecursively(dir2 + buildDir), 'dir2'); +sortFiles(readFilesRecursively(dir1 + sourcesOutput), 'dir1'); +sortFiles(readFilesRecursively(dir2 + sourcesOutput), 'dir2'); // console.log(Object.keys(files)) -const fileOut = displayDiffs(sections.dir1, sections.dir2, true) -console.log(fileOut) \ No newline at end of file +const fileOut = displayDiffs(sections.dir1, sections.dir2, true); +console.log(fileOut); diff --git a/.github/workflows/asana.yml b/.github/workflows/asana.yml index 0ab00debc..d536f61d0 100644 --- a/.github/workflows/asana.yml +++ b/.github/workflows/asana.yml @@ -1,23 +1,23 @@ name: 'asana sync' on: - pull_request_review: - pull_request_target: - types: - - opened - - edited - - closed - - reopened - - synchronize - - review_requested + pull_request_review: + pull_request_target: + types: + - opened + - edited + - closed + - reopened + - synchronize + - review_requested jobs: - sync: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: sammacbeth/action-asana-sync@v6 - with: - ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} - ASANA_WORKSPACE_ID: ${{ secrets.ASANA_WORKSPACE_ID }} - ASANA_PROJECT_ID: '1208598406046969' - USER_MAP: ${{ vars.USER_MAP }} + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: sammacbeth/action-asana-sync@v6 + with: + ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }} + ASANA_WORKSPACE_ID: ${{ secrets.ASANA_WORKSPACE_ID }} + ASANA_PROJECT_ID: '1208598406046969' + USER_MAP: ${{ vars.USER_MAP }} diff --git a/.github/workflows/auto-respond-pr.yml b/.github/workflows/auto-respond-pr.yml index 236672451..053322e13 100644 --- a/.github/workflows/auto-respond-pr.yml +++ b/.github/workflows/auto-respond-pr.yml @@ -2,83 +2,83 @@ name: Auto Respond to PR on: pull_request: - types: [opened, synchronize, closed, ready_for_review] + types: [opened, synchronize, closed, ready_for_review] jobs: - auto_respond: - if: github.actor != 'dependabot[bot]' - runs-on: ubuntu-latest + auto_respond: + if: github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest - steps: - - name: Checkout base branch - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.base.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - path: base + steps: + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + path: base - - name: Checkout PR branch - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - path: pr - fetch-depth: 0 + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + path: pr + fetch-depth: 0 - - name: Run build script on base branch - run: | - cd base - npm install - npm run build - cd .. + - name: Run build script on base branch + run: | + cd base + npm install + npm run build + cd .. - - name: Run build script on PR branch - run: | - cd pr - git config --global user.email "dax@duck.com" - git config --global user.name "dax" - echo ${{ github.event.pull_request.base.ref }} - git fetch origin ${{ github.event.pull_request.base.ref }} - git rebase origin/${{ github.event.pull_request.base.ref }} - npm install - npm run build - cd .. + - name: Run build script on PR branch + run: | + cd pr + git config --global user.email "dax@duck.com" + git config --global user.name "dax" + echo ${{ github.event.pull_request.base.ref }} + git fetch origin ${{ github.event.pull_request.base.ref }} + git rebase origin/${{ github.event.pull_request.base.ref }} + npm install + npm run build + cd .. - - name: Create diff of file outputs - run: | - node pr/.github/scripts/diff-directories.js base pr > diff.txt + - name: Create diff of file outputs + run: | + node pr/.github/scripts/diff-directories.js base pr > diff.txt - - name: Find Previous Comment - uses: peter-evans/find-comment@v3 - id: find_comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: 'Generated file diff' - direction: last + - name: Find Previous Comment + uses: peter-evans/find-comment@v3 + id: find_comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: 'Generated file diff' + direction: last - - name: Create Comment Body - uses: actions/github-script@v7 - id: create_body - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const prNumber = context.issue.number; - const diffOut = fs.readFileSync('diff.txt', 'utf8'); - const commentBody = ` - ### *[Beta]* Generated file diff - *Time updated:* ${new Date().toUTCString()} + - name: Create Comment Body + uses: actions/github-script@v7 + id: create_body + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const prNumber = context.issue.number; + const diffOut = fs.readFileSync('diff.txt', 'utf8'); + const commentBody = ` + ### *[Beta]* Generated file diff + *Time updated:* ${new Date().toUTCString()} - ${diffOut} - `; - core.setOutput('comment_body', commentBody); - core.setOutput('pr_number', prNumber); + ${diffOut} + `; + core.setOutput('comment_body', commentBody); + core.setOutput('pr_number', prNumber); - - name: Create, or Update the Comment - uses: peter-evans/create-or-update-comment@v4 - with: - issue-number: ${{ github.event.pull_request.number }} - comment-id: ${{ steps.find_comment.outputs.comment-id }} - body: ${{ steps.create_body.outputs.comment_body }} - edit-mode: replace + - name: Create, or Update the Comment + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + comment-id: ${{ steps.find_comment.outputs.comment-id }} + body: ${{ steps.create_body.outputs.comment_body }} + edit-mode: replace diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 5991d16f8..c038cfd72 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -1,105 +1,105 @@ name: PR Build and Release on: - pull_request: - types: [opened, synchronize, closed, ready_for_review] + pull_request: + types: [opened, synchronize, closed, ready_for_review] permissions: write-all jobs: - build: - if: github.event.action != 'closed' - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20.x - - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install dependencies - run: npm ci --verbose - - - name: Run build - run: npm run build - - - name: Create and push release branch - id: create_branch - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b pr-releases/pr-${PR_NUMBER} - git add -f build Sources - git commit -m "Add build folder for PR ${PR_NUMBER}" - git push -u origin pr-releases/pr-${PR_NUMBER} --force - echo "BRANCH_NAME=pr-releases/pr-${PR_NUMBER}" >> $GITHUB_ENV - echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV - - - name: Find Previous Comment - uses: peter-evans/find-comment@v3 - id: find_comment - with: - issue-number: ${{ github.event.pull_request.number }} - comment-author: 'github-actions[bot]' - body-includes: 'Temporary Branch Update' - direction: last - - - name: Create Comment Body - uses: actions/github-script@v7 - id: create_body - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const branchName = process.env.BRANCH_NAME; - const commitHash = process.env.COMMIT_HASH; - const prNumber = context.issue.number; - const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`; - const branchUrl = `${repoUrl}/tree/${branchName}`; - const commitUrl = `${repoUrl}/commit/${commitHash}`; - const commentBody = ` - ### Temporary Branch Update - - The temporary branch has been updated with the latest changes. Below are the details: - - - **Branch Name**: [${branchName}](${branchUrl}) - - **Commit Hash**: [${commitHash}](${commitUrl}) - - **Install Command**: \`npm i github:duckduckgo/content-scope-scripts#${commitHash}\` - - Please use the above install command to update to the latest version. - `; - core.setOutput('comment_body', commentBody); - core.setOutput('pr_number', prNumber); - - - name: Create, or Update the Comment - uses: peter-evans/create-or-update-comment@v4 - with: - issue-number: ${{ github.event.pull_request.number }} - comment-id: ${{ steps.find_comment.outputs.comment-id }} - body: ${{ steps.create_body.outputs.comment_body }} - edit-mode: replace - - clean_up: - if: github.event.action == 'closed' - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Delete release branch - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git push origin --delete pr-releases/pr-${PR_NUMBER} + build: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20.x + - uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm ci --verbose + + - name: Run build + run: npm run build + + - name: Create and push release branch + id: create_branch + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b pr-releases/pr-${PR_NUMBER} + git add -f build Sources + git commit -m "Add build folder for PR ${PR_NUMBER}" + git push -u origin pr-releases/pr-${PR_NUMBER} --force + echo "BRANCH_NAME=pr-releases/pr-${PR_NUMBER}" >> $GITHUB_ENV + echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_ENV + + - name: Find Previous Comment + uses: peter-evans/find-comment@v3 + id: find_comment + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: 'Temporary Branch Update' + direction: last + + - name: Create Comment Body + uses: actions/github-script@v7 + id: create_body + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const branchName = process.env.BRANCH_NAME; + const commitHash = process.env.COMMIT_HASH; + const prNumber = context.issue.number; + const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`; + const branchUrl = `${repoUrl}/tree/${branchName}`; + const commitUrl = `${repoUrl}/commit/${commitHash}`; + const commentBody = ` + ### Temporary Branch Update + + The temporary branch has been updated with the latest changes. Below are the details: + + - **Branch Name**: [${branchName}](${branchUrl}) + - **Commit Hash**: [${commitHash}](${commitUrl}) + - **Install Command**: \`npm i github:duckduckgo/content-scope-scripts#${commitHash}\` + + Please use the above install command to update to the latest version. + `; + core.setOutput('comment_body', commentBody); + core.setOutput('pr_number', prNumber); + + - name: Create, or Update the Comment + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + comment-id: ${{ steps.find_comment.outputs.comment-id }} + body: ${{ steps.create_body.outputs.comment_body }} + edit-mode: replace + + clean_up: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Delete release branch + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git push origin --delete pr-releases/pr-${PR_NUMBER} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d4a5934e..57309c991 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,88 +1,88 @@ name: Release on: - workflow_dispatch: - inputs: - version: - required: true - description: 'Release version' + workflow_dispatch: + inputs: + version: + required: true + description: 'Release version' jobs: - release_pr: - runs-on: ubuntu-latest + release_pr: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' - - name: Fetch files and ensure branches exist - run: | - git fetch origin - if [ -f .git/shallow ]; then - echo "Shallow repo clone, unshallowing" - git fetch --unshallow - fi - git fetch --tags - # Check if the 'main' branch exists, if not, create it - if git rev-parse --verify main >/dev/null 2>&1; then - git checkout main - else - git checkout -b main origin/main - fi - # Check if the 'releases' branch exists, if not, create it - if git rev-parse --verify releases >/dev/null 2>&1; then - git checkout releases - else - git checkout -b releases origin/releases - fi + - name: Fetch files and ensure branches exist + run: | + git fetch origin + if [ -f .git/shallow ]; then + echo "Shallow repo clone, unshallowing" + git fetch --unshallow + fi + git fetch --tags + # Check if the 'main' branch exists, if not, create it + if git rev-parse --verify main >/dev/null 2>&1; then + git checkout main + else + git checkout -b main origin/main + fi + # Check if the 'releases' branch exists, if not, create it + if git rev-parse --verify releases >/dev/null 2>&1; then + git checkout releases + else + git checkout -b releases origin/releases + fi - - name: Collect commit ranges - run: | - bash ./scripts/changelog.sh > ${{ github.workspace }}/CHANGELOG.txt + - name: Collect commit ranges + run: | + bash ./scripts/changelog.sh > ${{ github.workspace }}/CHANGELOG.txt - - name: Debug changelog file - run: | - ls -la ${{ github.workspace }}/CHANGELOG.txt - cat ${{ github.workspace }}/CHANGELOG.txt - echo "Current tag is: $(git rev-list --tags --max-count=1)" + - name: Debug changelog file + run: | + ls -la ${{ github.workspace }}/CHANGELOG.txt + cat ${{ github.workspace }}/CHANGELOG.txt + echo "Current tag is: $(git rev-list --tags --max-count=1)" - - name: Checkout code from main into release branch - run: | - # Checkout the code of main onto releases - git checkout main -- . + - name: Checkout code from main into release branch + run: | + # Checkout the code of main onto releases + git checkout main -- . - - name: Build release - run: | - npm ci - npm run build + - name: Build release + run: | + npm ci + npm run build - - name: Check in files - run: | - git add -f build/ Sources/ + - name: Check in files + run: | + git add -f build/ Sources/ - - name: Commit build files - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "Release build ${{ github.event.inputs.version }} [ci release]" - commit_options: '--allow-empty' - skip_checkout: true - branch: "releases" + - name: Commit build files + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: 'Release build ${{ github.event.inputs.version }} [ci release]' + commit_options: '--allow-empty' + skip_checkout: true + branch: 'releases' - - name: Debug changelog file - run: | - ls -la ${{ github.workspace }}/CHANGELOG.txt - cat ${{ github.workspace }}/CHANGELOG.txt + - name: Debug changelog file + run: | + ls -la ${{ github.workspace }}/CHANGELOG.txt + cat ${{ github.workspace }}/CHANGELOG.txt - - name: Create Release - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body_path: ${{ github.workspace }}/CHANGELOG.txt - draft: false - prerelease: false - tag_name: ${{ github.event.inputs.version }} - target_commitish: "releases" + - name: Create Release + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + body_path: ${{ github.workspace }}/CHANGELOG.txt + draft: false + prerelease: false + tag_name: ${{ github.event.inputs.version }} + target_commitish: 'releases' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 78b244c2b..56fa9795d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,54 +1,54 @@ -name: "CodeQL" +name: 'CodeQL' on: - push: - branches: [ develop ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ develop ] - schedule: - - cron: '40 11 * * 5' + push: + branches: [develop] + pull_request: + # The branches below must be a subset of the branches above + branches: [develop] + schedule: + - cron: '40 11 * * 5' jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript'] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c0fa120c8..231172445 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,106 +1,106 @@ name: Test on: - push: - branches: - - main - pull_request: + push: + branches: + - main + pull_request: permissions: - contents: read - pages: write - id-token: write - deployments: write + contents: read + pages: write + id-token: write + deployments: write jobs: - unit: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, windows-latest ] - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20.x - - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - run: npm ci - - run: npm run build - - run: npm run lint - - run: npm run stylelint - - run: npm run test-unit - - name: "Clean tree" - run: "npm run test-clean-tree" - integration: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20.x - - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - name: Install dependencies - run: | - npm install - npm run build - - name: Cache docs output - id: docs-output - uses: actions/cache@v4 - with: - path: docs - key: docs-output-${{ github.run_id }} - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Install dependencies for CI integration tests - run: sudo apt-get install xvfb - - run: npm run test-int-x - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report-pages - path: special-pages/test-results - retention-days: 5 - - name: Build docs - run: npm run docs + unit: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20.x + - uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npm ci + - run: npm run build + - run: npm run lint + - run: npm run stylelint + - run: npm run test-unit + - name: 'Clean tree' + run: 'npm run test-clean-tree' + integration: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20.x + - uses: actions/cache@v4 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Install dependencies + run: | + npm install + npm run build + - name: Cache docs output + id: docs-output + uses: actions/cache@v4 + with: + path: docs + key: docs-output-${{ github.run_id }} + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Install dependencies for CI integration tests + run: sudo apt-get install xvfb + - run: npm run test-int-x + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-pages + path: special-pages/test-results + retention-days: 5 + - name: Build docs + run: npm run docs - deploy-docs: - runs-on: ubuntu-latest - needs: integration - if: ${{ github.ref == 'refs/heads/main' }} - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20.x - - name: Cache build outputs - id: docs-output - uses: actions/cache@v4 - with: - path: docs - key: docs-output-${{ github.run_id }} - - name: Setup Github Pages - uses: actions/configure-pages@v5 - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: docs - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + deploy-docs: + runs-on: ubuntu-latest + needs: integration + if: ${{ github.ref == 'refs/heads/main' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Cache build outputs + id: docs-output + uses: actions/cache@v4 + with: + path: docs + key: docs-output-${{ github.run_id }} + - name: Setup Github Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 9fb776896..2f86b70f2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ test-results # Local Netlify folder .netlify +# VS Code user config +.vscode diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..c9ae3898a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +build/**/* +docs/**/* +injected/src/types +special-pages/types +injected/integration-test/extension/contentScope.js +**/*.json +**/*.md +**/*.html +**/*.har +**/*.css diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..05af754a1 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "printWidth": 140, + "tabWidth": 4 +} diff --git a/build-output.eslint.config.js b/build-output.eslint.config.js index 833e9a0c1..f9062ba5b 100644 --- a/build-output.eslint.config.js +++ b/build-output.eslint.config.js @@ -4,11 +4,11 @@ export default [ { languageOptions: { - ecmaVersion: "latest", - sourceType: "script", + ecmaVersion: 'latest', + sourceType: 'script', }, rules: { - "no-implicit-globals": "error", - } - } + 'no-implicit-globals': 'error', + }, + }, ]; diff --git a/eslint.config.js b/eslint.config.js index f470cff2c..f874a5295 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,6 +1,6 @@ import tseslint from 'typescript-eslint'; -import ddgConfig from "@duckduckgo/eslint-config"; -import globals from "globals"; +import ddgConfig from '@duckduckgo/eslint-config'; +import globals from 'globals'; // @ts-check export default tseslint.config( @@ -8,98 +8,104 @@ export default tseslint.config( ...tseslint.configs.recommended, { ignores: [ - "**/build/", - "**/docs/", - "injected/lib", - "Sources/ContentScopeScripts/dist/", - "injected/integration-test/extension/contentScope.js", - "injected/integration-test/test-pages/duckplayer/scripts/dist", - "special-pages/pages/**/public", - "special-pages/playwright-report/", - "special-pages/test-results/", - "special-pages/types/", - "special-pages/messages/", - "playwright-report", - "test-results", - "injected/src/types", - ".idea" + '**/build/', + '**/docs/', + 'injected/lib', + 'Sources/ContentScopeScripts/dist/', + 'injected/integration-test/extension/contentScope.js', + 'injected/integration-test/test-pages/duckplayer/scripts/dist', + 'special-pages/pages/**/public', + 'special-pages/playwright-report/', + 'special-pages/test-results/', + 'special-pages/types/', + 'special-pages/messages/', + 'playwright-report', + 'test-results', + 'injected/src/types', + '.idea', ], }, { languageOptions: { globals: { - $USER_PREFERENCES$: "readonly", - $USER_UNPROTECTED_DOMAINS$: "readonly", - $CONTENT_SCOPE$: "readonly", - $BUNDLED_CONFIG$: "readonly", + $USER_PREFERENCES$: 'readonly', + $USER_UNPROTECTED_DOMAINS$: 'readonly', + $CONTENT_SCOPE$: 'readonly', + $BUNDLED_CONFIG$: 'readonly', }, - ecmaVersion: "latest", - sourceType: "script", + ecmaVersion: 'latest', + sourceType: 'script', }, rules: { - "no-restricted-syntax": ["error", { - selector: "MethodDefinition[key.type='PrivateIdentifier']", - message: "Private methods are currently unsupported in older WebKit and ESR Firefox", - }], + 'no-restricted-syntax': [ + 'error', + { + selector: "MethodDefinition[key.type='PrivateIdentifier']", + message: 'Private methods are currently unsupported in older WebKit and ESR Firefox', + }, + ], - "require-await": ["error"], - "promise/prefer-await-to-then": ["error"], - "@typescript-eslint/no-unused-vars": ["error", { - args: "none", - caughtErrors: "none", - ignoreRestSiblings: true, - vars: "all" - }], + 'require-await': ['error'], + 'promise/prefer-await-to-then': ['error'], + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'none', + caughtErrors: 'none', + ignoreRestSiblings: true, + vars: 'all', + }, + ], }, }, { - ignores: ["injected/integration-test/test-pages/**", "injected/integration-test/extension/**"], + ignores: ['injected/integration-test/test-pages/**', 'injected/integration-test/extension/**'], languageOptions: { parserOptions: { projectService: { allowDefaultProject: ['eslint.config.js', 'build-output.eslint.config.js'], }, - } + }, }, rules: { - "@typescript-eslint/await-thenable": "error", + '@typescript-eslint/await-thenable': 'error', }, }, { - files: ["**/scripts/*.js", "**/*.mjs", "**/unit-test/**/*.js", "**/integration-test/**/*.spec.js"], + files: ['**/scripts/*.js', '**/*.mjs', '**/unit-test/**/*.js', '**/integration-test/**/*.spec.js'], languageOptions: { globals: { ...globals.node, - } - } + }, + }, }, { - files: ["injected/**/*.js"], + files: ['injected/**/*.js'], languageOptions: { globals: { - windowsInteropPostMessage: "readonly", - windowsInteropAddEventListener: "readonly", - windowsInteropRemoveEventListener: "readonly", - } - } + windowsInteropPostMessage: 'readonly', + windowsInteropAddEventListener: 'readonly', + windowsInteropRemoveEventListener: 'readonly', + }, + }, }, { - files: ["**/unit-test/*.js"], + files: ['**/unit-test/*.js'], languageOptions: { globals: { ...globals.jasmine, - } - } + }, + }, }, { - ignores: ["**/scripts/*.js"], + ignores: ['**/scripts/*.js'], languageOptions: { globals: { ...globals.browser, ...globals.webextensions, - } - } - } + }, + }, + }, ); diff --git a/injected/entry-points/android.js b/injected/entry-points/android.js index f3f5e69f0..e3f3506c2 100644 --- a/injected/entry-points/android.js +++ b/injected/entry-points/android.js @@ -1,29 +1,29 @@ /** * @module Android integration */ -import { load, init } from '../src/content-scope-features.js' -import { processConfig, isGloballyDisabled } from './../src/utils' -import { isTrackerOrigin } from '../src/trackers' -import { AndroidMessagingConfig } from '../../messaging/index.js' +import { load, init } from '../src/content-scope-features.js'; +import { processConfig, isGloballyDisabled } from './../src/utils'; +import { isTrackerOrigin } from '../src/trackers'; +import { AndroidMessagingConfig } from '../../messaging/index.js'; -function initCode () { +function initCode() { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const processedConfig = processConfig($CONTENT_SCOPE$, $USER_UNPROTECTED_DOMAINS$, $USER_PREFERENCES$) + const processedConfig = processConfig($CONTENT_SCOPE$, $USER_UNPROTECTED_DOMAINS$, $USER_PREFERENCES$); if (isGloballyDisabled(processedConfig)) { - return + return; } - const configConstruct = processedConfig - const messageCallback = configConstruct.messageCallback - const messageSecret = configConstruct.messageSecret - const javascriptInterface = configConstruct.javascriptInterface + const configConstruct = processedConfig; + const messageCallback = configConstruct.messageCallback; + const messageSecret = configConstruct.messageSecret; + const javascriptInterface = configConstruct.javascriptInterface; processedConfig.messagingConfig = new AndroidMessagingConfig({ messageSecret, messageCallback, javascriptInterface, target: globalThis, - debug: processedConfig.debug - }) + debug: processedConfig.debug, + }); load({ platform: processedConfig.platform, @@ -31,10 +31,10 @@ function initCode () { documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup), site: processedConfig.site, bundledConfig: processedConfig.bundledConfig, - messagingConfig: processedConfig.messagingConfig - }) + messagingConfig: processedConfig.messagingConfig, + }); - init(processedConfig) + init(processedConfig); } -initCode() +initCode(); diff --git a/injected/entry-points/apple.js b/injected/entry-points/apple.js index e95a23c12..c53ad4bd7 100644 --- a/injected/entry-points/apple.js +++ b/injected/entry-points/apple.js @@ -1,38 +1,38 @@ /** * @module Apple integration */ -import { load, init } from '../src/content-scope-features.js' -import { processConfig, isGloballyDisabled } from './../src/utils' -import { isTrackerOrigin } from '../src/trackers' -import { WebkitMessagingConfig, TestTransportConfig } from '../../messaging/index.js' +import { load, init } from '../src/content-scope-features.js'; +import { processConfig, isGloballyDisabled } from './../src/utils'; +import { isTrackerOrigin } from '../src/trackers'; +import { WebkitMessagingConfig, TestTransportConfig } from '../../messaging/index.js'; -function initCode () { +function initCode() { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const processedConfig = processConfig($CONTENT_SCOPE$, $USER_UNPROTECTED_DOMAINS$, $USER_PREFERENCES$) + const processedConfig = processConfig($CONTENT_SCOPE$, $USER_UNPROTECTED_DOMAINS$, $USER_PREFERENCES$); if (isGloballyDisabled(processedConfig)) { - return + return; } if (import.meta.injectName === 'apple-isolated') { processedConfig.messagingConfig = new WebkitMessagingConfig({ webkitMessageHandlerNames: ['contentScopeScriptsIsolated'], secret: '', - hasModernWebkitAPI: true - }) + hasModernWebkitAPI: true, + }); } else { processedConfig.messagingConfig = new TestTransportConfig({ - notify () { + notify() { // noop }, request: async () => { // noop }, - subscribe () { + subscribe() { return () => { // noop - } - } - }) + }; + }, + }); } load({ @@ -41,13 +41,13 @@ function initCode () { documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup), site: processedConfig.site, bundledConfig: processedConfig.bundledConfig, - messagingConfig: processedConfig.messagingConfig - }) + messagingConfig: processedConfig.messagingConfig, + }); - init(processedConfig) + init(processedConfig); // Not supported: // update(message) } -initCode() +initCode(); diff --git a/injected/entry-points/chrome-mv3.js b/injected/entry-points/chrome-mv3.js index dd93f1f30..7a2b28943 100644 --- a/injected/entry-points/chrome-mv3.js +++ b/injected/entry-points/chrome-mv3.js @@ -1,42 +1,44 @@ /** * @module Chrome MV3 integration */ -import { load, init, update } from '../src/content-scope-features.js' -import { isTrackerOrigin } from '../src/trackers' -import { computeLimitedSiteObject } from '../src/utils.js' +import { load, init, update } from '../src/content-scope-features.js'; +import { isTrackerOrigin } from '../src/trackers'; +import { computeLimitedSiteObject } from '../src/utils.js'; -const secret = (crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32).toString().replace('0.', '') +const secret = (crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32).toString().replace('0.', ''); -const trackerLookup = import.meta.trackerLookup +const trackerLookup = import.meta.trackerLookup; load({ platform: { - name: 'extension' + name: 'extension', }, trackerLookup, documentOriginIsTracker: isTrackerOrigin(trackerLookup), site: computeLimitedSiteObject(), // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - bundledConfig: $BUNDLED_CONFIG$ -}) + bundledConfig: $BUNDLED_CONFIG$, +}); // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f window.addEventListener(secret, ({ detail: message }) => { - if (!message) return + if (!message) return; switch (message.type) { - case 'update': - update(message) - break - case 'register': - if (message.argumentsObject) { - message.argumentsObject.messageSecret = secret - init(message.argumentsObject) - } - break + case 'update': + update(message); + break; + case 'register': + if (message.argumentsObject) { + message.argumentsObject.messageSecret = secret; + init(message.argumentsObject); + } + break; } -}) +}); -window.dispatchEvent(new CustomEvent('ddg-secret', { - detail: secret -})) +window.dispatchEvent( + new CustomEvent('ddg-secret', { + detail: secret, + }), +); diff --git a/injected/entry-points/chrome.js b/injected/entry-points/chrome.js index 247c51c0a..03a91c63a 100644 --- a/injected/entry-points/chrome.js +++ b/injected/entry-points/chrome.js @@ -1,8 +1,8 @@ /** * @module Chrome integration */ -import { isTrackerOrigin } from '../src/trackers' -import { computeLimitedSiteObject } from '../src/utils' +import { isTrackerOrigin } from '../src/trackers'; +import { computeLimitedSiteObject } from '../src/utils'; /** * Inject all the overwrites into the page. @@ -16,39 +16,38 @@ const allowedMessages = [ 'setYoutubePreviewsEnabled', 'unblockClickToLoadContent', 'updateYouTubeCTLAddedFlag', - 'updateFacebookCTLBreakageFlags' -] -const messageSecret = randomString() + 'updateFacebookCTLBreakageFlags', +]; +const messageSecret = randomString(); -function inject (code) { - const elem = document.head || document.documentElement +function inject(code) { + const elem = document.head || document.documentElement; // Inject into main page try { - const e = document.createElement('script') + const e = document.createElement('script'); e.textContent = `(() => { ${code} - })();` - elem.appendChild(e) - e.remove() - } catch (e) { - } + })();`; + elem.appendChild(e); + e.remove(); + } catch (e) {} } -function randomString () { - const num = crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32 - return num.toString().replace('0.', '') +function randomString() { + const num = crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32; + return num.toString().replace('0.', ''); } -function init () { - const trackerLookup = import.meta.trackerLookup - const documentOriginIsTracker = isTrackerOrigin(trackerLookup) +function init() { + const trackerLookup = import.meta.trackerLookup; + const documentOriginIsTracker = isTrackerOrigin(trackerLookup); // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const bundledConfig = $BUNDLED_CONFIG$ - const randomMethodName = '_d' + randomString() - const randomPassword = '_p' + randomString() - const reusableMethodName = '_rm' + randomString() - const reusableSecret = '_r' + randomString() - const siteObject = computeLimitedSiteObject() + const bundledConfig = $BUNDLED_CONFIG$; + const randomMethodName = '_d' + randomString(); + const randomPassword = '_p' + randomString(); + const reusableMethodName = '_rm' + randomString(); + const reusableSecret = '_r' + randomString(); + const siteObject = computeLimitedSiteObject(); const initialScript = ` /* global contentScopeFeatures */ contentScopeFeatures.load({ @@ -97,72 +96,74 @@ function init () { } }) }); - ` - inject(initialScript) + `; + inject(initialScript); - chrome.runtime.sendMessage({ - messageType: 'registeredContentScript', - options: { - documentUrl: window.location.href - } - }, - (message) => { - if (!message) { - // Remove injected function only as background has disabled feature - inject(`delete window.${randomMethodName}`) - return - } - if (message.debug) { - window.addEventListener('message', (m) => { - if (m.data.action && m.data.message) { - chrome.runtime.sendMessage({ messageType: 'debuggerMessage', options: m.data }) - } - }) - } - message.messageSecret = messageSecret - const stringifiedArgs = JSON.stringify(message) - const callRandomFunction = ` + chrome.runtime.sendMessage( + { + messageType: 'registeredContentScript', + options: { + documentUrl: window.location.href, + }, + }, + (message) => { + if (!message) { + // Remove injected function only as background has disabled feature + inject(`delete window.${randomMethodName}`); + return; + } + if (message.debug) { + window.addEventListener('message', (m) => { + if (m.data.action && m.data.message) { + chrome.runtime.sendMessage({ messageType: 'debuggerMessage', options: m.data }); + } + }); + } + message.messageSecret = messageSecret; + const stringifiedArgs = JSON.stringify(message); + const callRandomFunction = ` window.${randomMethodName}('${randomPassword}', ${stringifiedArgs}); - ` - inject(callRandomFunction) - }) + `; + inject(callRandomFunction); + }, + ); chrome.runtime.onMessage.addListener((message) => { // forward update messages to the embedded script if (message && message.type === 'update') { - const stringifiedArgs = JSON.stringify(message) + const stringifiedArgs = JSON.stringify(message); const callRandomUpdateFunction = ` window.${reusableMethodName}('${reusableSecret}', ${stringifiedArgs}); - ` - inject(callRandomUpdateFunction) + `; + inject(callRandomUpdateFunction); } - }) + }); - window.addEventListener('sendMessageProxy' + messageSecret, event => { - event.stopImmediatePropagation() + window.addEventListener('sendMessageProxy' + messageSecret, (event) => { + event.stopImmediatePropagation(); if (!(event instanceof CustomEvent) || !event?.detail) { - return console.warn('no details in sendMessage proxy', event) + return console.warn('no details in sendMessage proxy', event); } - const messageType = event.detail?.messageType + const messageType = event.detail?.messageType; if (!allowedMessages.includes(messageType)) { - return console.warn('Ignoring invalid sendMessage messageType', messageType) + return console.warn('Ignoring invalid sendMessage messageType', messageType); } - chrome.runtime.sendMessage(event.detail, response => { + chrome.runtime.sendMessage(event.detail, (response) => { const message = { messageType: 'response', responseMessageType: messageType, - response - } - const stringifiedArgs = JSON.stringify(message) + response, + }; + const stringifiedArgs = JSON.stringify(message); const callRandomUpdateFunction = ` window.${reusableMethodName}('${reusableSecret}', ${stringifiedArgs}); - ` - inject(callRandomUpdateFunction) - }) - }) + `; + inject(callRandomUpdateFunction); + }); + }); } -init() +init(); diff --git a/injected/entry-points/integration.js b/injected/entry-points/integration.js index 22f7836e2..fe43e49e7 100644 --- a/injected/entry-points/integration.js +++ b/injected/entry-points/integration.js @@ -1,42 +1,37 @@ -import { load, init } from '../src/content-scope-features.js' -import { isTrackerOrigin } from '../src/trackers' -import { TestTransportConfig } from '../../messaging/index.js' -function getTopLevelURL () { +import { load, init } from '../src/content-scope-features.js'; +import { isTrackerOrigin } from '../src/trackers'; +import { TestTransportConfig } from '../../messaging/index.js'; +function getTopLevelURL() { try { // FROM: https://stackoverflow.com/a/7739035/73479 // FIX: Better capturing of top level URL so that trackers in embedded documents are not considered first party if (window.location !== window.parent.location) { - return new URL(window.location.href !== 'about:blank' ? document.referrer : window.parent.location.href) + return new URL(window.location.href !== 'about:blank' ? document.referrer : window.parent.location.href); } else { - return new URL(window.location.href) + return new URL(window.location.href); } } catch (error) { - return new URL(location.href) + return new URL(location.href); } } -function generateConfig () { - const topLevelUrl = getTopLevelURL() - const trackerLookup = import.meta.trackerLookup +function generateConfig() { + const topLevelUrl = getTopLevelURL(); + const trackerLookup = import.meta.trackerLookup; return { debug: false, sessionKey: 'randomVal', platform: { - name: 'extension' + name: 'extension', }, site: { domain: topLevelUrl.hostname, isBroken: false, allowlisted: false, - enabledFeatures: [ - 'fingerprintingCanvas', - 'fingerprintingScreenSize', - 'navigatorInterface', - 'cookie' - ] + enabledFeatures: ['fingerprintingCanvas', 'fingerprintingScreenSize', 'navigatorInterface', 'cookie'], }, - trackerLookup - } + trackerLookup, + }; } /** @@ -44,8 +39,8 @@ function generateConfig () { * @param item * @returns {boolean} */ -function isObject (item) { - return (item && typeof item === 'object' && !Array.isArray(item)) +function isObject(item) { + return item && typeof item === 'object' && !Array.isArray(item); } /** @@ -53,78 +48,82 @@ function isObject (item) { * @param target * @param sources */ -function mergeDeep (target, ...sources) { - if (!sources.length) return target - const source = sources.shift() +function mergeDeep(target, ...sources) { + if (!sources.length) return target; + const source = sources.shift(); if (isObject(target) && isObject(source)) { for (const key in source) { if (isObject(source[key])) { - if (!target[key]) Object.assign(target, { [key]: {} }) - mergeDeep(target[key], source[key]) + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); } else { - Object.assign(target, { [key]: source[key] }) + Object.assign(target, { [key]: source[key] }); } } } - return mergeDeep(target, ...sources) + return mergeDeep(target, ...sources); } -async function initCode () { - const topLevelUrl = getTopLevelURL() - const processedConfig = generateConfig() +async function initCode() { + const topLevelUrl = getTopLevelURL(); + const processedConfig = generateConfig(); // mock Messaging and allow for tests to intercept them globalThis.cssMessaging = processedConfig.messagingConfig = new TestTransportConfig({ - notify () { + notify() { // noop }, request: async () => { // noop }, - subscribe () { + subscribe() { return () => { // noop - } - } - }) + }; + }, + }); load({ // @ts-expect-error Types of property 'name' are incompatible. platform: processedConfig.platform, trackerLookup: processedConfig.trackerLookup, documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup), site: processedConfig.site, - messagingConfig: processedConfig.messagingConfig - }) + messagingConfig: processedConfig.messagingConfig, + }); // mark this phase as loaded - setStatus('loaded') + setStatus('loaded'); if (!topLevelUrl.searchParams.has('wait-for-init-args')) { - await init(processedConfig) - setStatus('initialized') - return + await init(processedConfig); + setStatus('initialized'); + return; } // Wait for a message containing additional config - document.addEventListener('content-scope-init-args', async (evt) => { - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const merged = mergeDeep(processedConfig, evt.detail) - // init features - await init(merged) + document.addEventListener( + 'content-scope-init-args', + async (evt) => { + // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f + const merged = mergeDeep(processedConfig, evt.detail); + // init features + await init(merged); - // set status to initialized so that tests can resume - setStatus('initialized') - }, { once: true }) + // set status to initialized so that tests can resume + setStatus('initialized'); + }, + { once: true }, + ); } /** * @param {"loaded" | "initialized"} status */ -function setStatus (status) { +function setStatus(status) { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - window.__content_scope_status = status + window.__content_scope_status = status; } -initCode() +initCode(); diff --git a/injected/entry-points/mozilla.js b/injected/entry-points/mozilla.js index e4a6543d1..e85b8ed1e 100644 --- a/injected/entry-points/mozilla.js +++ b/injected/entry-points/mozilla.js @@ -1,9 +1,9 @@ /** * @module Mozilla integration */ -import { load, init, update } from '../src/content-scope-features.js' -import { isTrackerOrigin } from '../src/trackers' -import { computeLimitedSiteObject } from '../src/utils.js' +import { load, init, update } from '../src/content-scope-features.js'; +import { isTrackerOrigin } from '../src/trackers'; +import { computeLimitedSiteObject } from '../src/utils.js'; const allowedMessages = [ 'getClickToLoadState', @@ -13,82 +13,84 @@ const allowedMessages = [ 'setYoutubePreviewsEnabled', 'unblockClickToLoadContent', 'updateYouTubeCTLAddedFlag', - 'updateFacebookCTLBreakageFlags' -] -const messageSecret = randomString() + 'updateFacebookCTLBreakageFlags', +]; +const messageSecret = randomString(); -function randomString () { - const num = crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32 - return num.toString().replace('0.', '') +function randomString() { + const num = crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32; + return num.toString().replace('0.', ''); } -function initCode () { - const trackerLookup = import.meta.trackerLookup +function initCode() { + const trackerLookup = import.meta.trackerLookup; load({ platform: { - name: 'extension' + name: 'extension', }, trackerLookup, documentOriginIsTracker: isTrackerOrigin(trackerLookup), site: computeLimitedSiteObject(), // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - bundledConfig: $BUNDLED_CONFIG$ - }) + bundledConfig: $BUNDLED_CONFIG$, + }); - chrome.runtime.sendMessage({ - messageType: 'registeredContentScript', - options: { - documentUrl: window.location.href - } - }, - (message) => { - // Background has disabled features - if (!message) { - return - } - if (message.debug) { - window.addEventListener('message', (m) => { - if (m.data.action && m.data.message) { - chrome.runtime.sendMessage({ - messageType: 'debuggerMessage', - options: m.data - }) - } - }) - } - message.messageSecret = messageSecret - init(message) - }) + chrome.runtime.sendMessage( + { + messageType: 'registeredContentScript', + options: { + documentUrl: window.location.href, + }, + }, + (message) => { + // Background has disabled features + if (!message) { + return; + } + if (message.debug) { + window.addEventListener('message', (m) => { + if (m.data.action && m.data.message) { + chrome.runtime.sendMessage({ + messageType: 'debuggerMessage', + options: m.data, + }); + } + }); + } + message.messageSecret = messageSecret; + init(message); + }, + ); chrome.runtime.onMessage.addListener((message) => { // forward update messages to the embedded script if (message && message.type === 'update') { - update(message) + update(message); } - }) + }); - window.addEventListener('sendMessageProxy' + messageSecret, event => { - event.stopImmediatePropagation() + window.addEventListener('sendMessageProxy' + messageSecret, (event) => { + event.stopImmediatePropagation(); if (!(event instanceof CustomEvent) || !event?.detail) { - return console.warn('no details in sendMessage proxy', event) + return console.warn('no details in sendMessage proxy', event); } - const messageType = event.detail?.messageType + const messageType = event.detail?.messageType; if (!allowedMessages.includes(messageType)) { - return console.warn('Ignoring invalid sendMessage messageType', messageType) + return console.warn('Ignoring invalid sendMessage messageType', messageType); } - chrome.runtime.sendMessage(event.detail, response => { + chrome.runtime.sendMessage(event.detail, (response) => { const message = { messageType: 'response', responseMessageType: messageType, - response - } + response, + }; - update(message) - }) - }) + update(message); + }); + }); } -initCode() +initCode(); diff --git a/injected/entry-points/windows.js b/injected/entry-points/windows.js index 5337cfde1..e5c7d561d 100644 --- a/injected/entry-points/windows.js +++ b/injected/entry-points/windows.js @@ -1,16 +1,16 @@ /** * @module Windows integration */ -import { load, init } from '../src/content-scope-features.js' -import { processConfig, isGloballyDisabled, windowsSpecificFeatures } from './../src/utils' -import { isTrackerOrigin } from '../src/trackers' -import { WindowsMessagingConfig } from '../../messaging/index.js' +import { load, init } from '../src/content-scope-features.js'; +import { processConfig, isGloballyDisabled, windowsSpecificFeatures } from './../src/utils'; +import { isTrackerOrigin } from '../src/trackers'; +import { WindowsMessagingConfig } from '../../messaging/index.js'; -function initCode () { +function initCode() { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const processedConfig = processConfig($CONTENT_SCOPE$, $USER_UNPROTECTED_DOMAINS$, $USER_PREFERENCES$, windowsSpecificFeatures) + const processedConfig = processConfig($CONTENT_SCOPE$, $USER_UNPROTECTED_DOMAINS$, $USER_PREFERENCES$, windowsSpecificFeatures); if (isGloballyDisabled(processedConfig)) { - return + return; } processedConfig.messagingConfig = new WindowsMessagingConfig({ methods: { @@ -19,9 +19,9 @@ function initCode () { // @ts-expect-error - Type 'unknown' is not assignable to type... addEventListener: windowsInteropAddEventListener, // @ts-expect-error - Type 'unknown' is not assignable to type... - removeEventListener: windowsInteropRemoveEventListener - } - }) + removeEventListener: windowsInteropRemoveEventListener, + }, + }); load({ platform: processedConfig.platform, @@ -29,13 +29,13 @@ function initCode () { documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup), site: processedConfig.site, bundledConfig: processedConfig.bundledConfig, - messagingConfig: processedConfig.messagingConfig - }) + messagingConfig: processedConfig.messagingConfig, + }); - init(processedConfig) + init(processedConfig); // Not supported: // update(message) } -initCode() +initCode(); diff --git a/injected/integration-test/autofill-password-import.spec.js b/injected/integration-test/autofill-password-import.spec.js index 3957d92cf..7a0e7d9c3 100644 --- a/injected/integration-test/autofill-password-import.spec.js +++ b/injected/integration-test/autofill-password-import.spec.js @@ -1,49 +1,46 @@ -import { test } from '@playwright/test' -import { readFileSync } from 'fs' -import { - mockAndroidMessaging, - wrapWebkitScripts -} from '@duckduckgo/messaging/lib/test-utils.mjs' -import { perPlatform } from './type-helpers.mjs' +import { test } from '@playwright/test'; +import { readFileSync } from 'fs'; +import { mockAndroidMessaging, wrapWebkitScripts } from '@duckduckgo/messaging/lib/test-utils.mjs'; +import { perPlatform } from './type-helpers.mjs'; test('Password import feature', async ({ page }, testInfo) => { - const passwordImportFeature = AutofillPasswordImportSpec.create(page, testInfo) - await passwordImportFeature.enabled() - await passwordImportFeature.navigate() - const didAnimatePasswordOptions = passwordImportFeature.waitForAnimation('a[aria-label="Password options"]') - await passwordImportFeature.clickOnElement('Home page') - await didAnimatePasswordOptions + const passwordImportFeature = AutofillPasswordImportSpec.create(page, testInfo); + await passwordImportFeature.enabled(); + await passwordImportFeature.navigate(); + const didAnimatePasswordOptions = passwordImportFeature.waitForAnimation('a[aria-label="Password options"]'); + await passwordImportFeature.clickOnElement('Home page'); + await didAnimatePasswordOptions; - const didAnimateSignin = passwordImportFeature.waitForAnimation('a[aria-label="Sign in"]') - await passwordImportFeature.clickOnElement('Signin page') - await didAnimateSignin + const didAnimateSignin = passwordImportFeature.waitForAnimation('a[aria-label="Sign in"]'); + await passwordImportFeature.clickOnElement('Signin page'); + await didAnimateSignin; - const didAnimateExport = passwordImportFeature.waitForAnimation('button[aria-label="Export"]') - await passwordImportFeature.clickOnElement('Export page') - await didAnimateExport -}) + const didAnimateExport = passwordImportFeature.waitForAnimation('button[aria-label="Export"]'); + await passwordImportFeature.clickOnElement('Export page'); + await didAnimateExport; +}); export class AutofillPasswordImportSpec { - htmlPage = '/autofill-password-import/index.html' - config = './integration-test/test-pages/autofill-password-import/config/config.json' + htmlPage = '/autofill-password-import/index.html'; + config = './integration-test/test-pages/autofill-password-import/config/config.json'; /** * @param {import("@playwright/test").Page} page * @param {import("./type-helpers.mjs").Build} build * @param {import("./type-helpers.mjs").PlatformInfo} platform */ - constructor (page, build, platform) { - this.page = page - this.build = build - this.platform = platform + constructor(page, build, platform) { + this.page = page; + this.build = build; + this.platform = platform; } - async enabled () { - const config = JSON.parse(readFileSync(this.config, 'utf8')) - await this.setup({ config }) + async enabled() { + const config = JSON.parse(readFileSync(this.config, 'utf8')); + await this.setup({ config }); } - async navigate () { - await this.page.goto(this.htmlPage) + async navigate() { + await this.page.goto(this.htmlPage); } /** @@ -51,8 +48,8 @@ export class AutofillPasswordImportSpec { * @param {Record} params.config * @return {Promise} */ - async setup (params) { - const { config } = params + async setup(params) { + const { config } = params; // read the built file from disk and do replacements const injectedJS = wrapWebkitScripts(this.build.artifact, { @@ -63,22 +60,22 @@ export class AutofillPasswordImportSpec { debug: true, javascriptInterface: '', messageCallback: '', - sessionKey: '' - } - }) + sessionKey: '', + }, + }); await this.page.addInitScript(mockAndroidMessaging, { messagingContext: { env: 'development', context: 'contentScopeScripts', - featureName: 'n/a' + featureName: 'n/a', }, responses: {}, - messageCallback: '' - }) + messageCallback: '', + }); // attach the JS - await this.page.addInitScript(injectedJS) + await this.page.addInitScript(injectedJS); } /** @@ -86,33 +83,33 @@ export class AutofillPasswordImportSpec { * @param {import("@playwright/test").Page} page * @param {import("@playwright/test").TestInfo} testInfo */ - static create (page, testInfo) { + static create(page, testInfo) { // Read the configuration object to determine which platform we're testing against - const { platformInfo, build } = perPlatform(testInfo.project.use) - return new AutofillPasswordImportSpec(page, build, platformInfo) + const { platformInfo, build } = perPlatform(testInfo.project.use); + return new AutofillPasswordImportSpec(page, build, platformInfo); } /** * Helper to assert that an element is animating * @param {string} selector */ - async waitForAnimation (selector) { - const locator = this.page.locator(selector) + async waitForAnimation(selector) { + const locator = this.page.locator(selector); return await locator.evaluate((el) => { if (el != null) { - return el.getAnimations().some((animation) => animation.playState === 'running') + return el.getAnimations().some((animation) => animation.playState === 'running'); } else { - return false + return false; } - }, selector) + }, selector); } /** * Helper to click on a button accessed via the aria-label attrbitue * @param {string} text */ - async clickOnElement (text) { - const element = this.page.getByText(text) - await element.click() + async clickOnElement(text) { + const element = this.page.getByText(text); + await element.click(); } } diff --git a/injected/integration-test/breakage-reporting.spec.js b/injected/integration-test/breakage-reporting.spec.js index e9d953966..4b16c0d64 100644 --- a/injected/integration-test/breakage-reporting.spec.js +++ b/injected/integration-test/breakage-reporting.spec.js @@ -1,67 +1,73 @@ -import { test, expect } from '@playwright/test' -import { readFileSync } from 'fs' +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'fs'; import { mockWindowsMessaging, - readOutgoingMessages, simulateSubscriptionMessage, waitForCallCount, - wrapWindowsScripts -} from '@duckduckgo/messaging/lib/test-utils.mjs' -import { perPlatform } from './type-helpers.mjs' + readOutgoingMessages, + simulateSubscriptionMessage, + waitForCallCount, + wrapWindowsScripts, +} from '@duckduckgo/messaging/lib/test-utils.mjs'; +import { perPlatform } from './type-helpers.mjs'; test('Breakage Reporting Feature', async ({ page }, testInfo) => { - const breakageFeature = BreakageReportingSpec.create(page, testInfo) - await breakageFeature.enabled() - await breakageFeature.navigate() + const breakageFeature = BreakageReportingSpec.create(page, testInfo); + await breakageFeature.enabled(); + await breakageFeature.navigate(); await page.evaluate(simulateSubscriptionMessage, { messagingContext: { context: 'contentScopeScripts', featureName: 'breakageReporting', - env: 'development' + env: 'development', }, name: 'getBreakageReportValues', payload: {}, - injectName: breakageFeature.build.name - }) + injectName: breakageFeature.build.name, + }); - await page.waitForFunction(waitForCallCount, { - method: 'breakageReportResult', - count: 1 - }, { timeout: 5000, polling: 100 }) - const calls = await page.evaluate(readOutgoingMessages) - expect(calls.length).toBe(1) + await page.waitForFunction( + waitForCallCount, + { + method: 'breakageReportResult', + count: 1, + }, + { timeout: 5000, polling: 100 }, + ); + const calls = await page.evaluate(readOutgoingMessages); + expect(calls.length).toBe(1); - const result = calls[0].payload.params - expect(result.jsPerformance.length).toBe(1) - expect(result.jsPerformance[0]).toBeGreaterThan(0) - expect(result.referrer).toBe('http://localhost:3220/breakage-reporting/index.html') -}) + const result = calls[0].payload.params; + expect(result.jsPerformance.length).toBe(1); + expect(result.jsPerformance[0]).toBeGreaterThan(0); + expect(result.referrer).toBe('http://localhost:3220/breakage-reporting/index.html'); +}); export class BreakageReportingSpec { - htmlPage = '/breakage-reporting/index.html' - config = './integration-test/test-pages/breakage-reporting/config/config.json' + htmlPage = '/breakage-reporting/index.html'; + config = './integration-test/test-pages/breakage-reporting/config/config.json'; /** * @param {import("@playwright/test").Page} page * @param {import("./type-helpers.mjs").Build} build * @param {import("./type-helpers.mjs").PlatformInfo} platform */ - constructor (page, build, platform) { - this.page = page - this.build = build - this.platform = platform + constructor(page, build, platform) { + this.page = page; + this.build = build; + this.platform = platform; } - async enabled () { - const config = JSON.parse(readFileSync(this.config, 'utf8')) - await this.setup({ config }) + async enabled() { + const config = JSON.parse(readFileSync(this.config, 'utf8')); + await this.setup({ config }); } - async navigate () { - await this.page.goto(this.htmlPage) + async navigate() { + await this.page.goto(this.htmlPage); await this.page.evaluate(() => { - window.location.href = '/breakage-reporting/pages/ref.html' - }) - await this.page.waitForURL('**/ref.html') + window.location.href = '/breakage-reporting/pages/ref.html'; + }); + await this.page.waitForURL('**/ref.html'); // Wait for first paint event to ensure we can get the performance metrics await this.page.evaluate(() => { @@ -69,17 +75,17 @@ export class BreakageReportingSpec { const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (entry.name === 'first-paint') { - observer.disconnect() + observer.disconnect(); // @ts-expect-error - error TS2810: Expected 1 argument, but got 0. 'new Promise()' needs a JSDoc hint to produce a 'resolve' that can be called without arguments. - resolve() + resolve(); } - }) - }) + }); + }); - observer.observe({ type: 'paint', buffered: true }) - }) - return response - }) + observer.observe({ type: 'paint', buffered: true }); + }); + return response; + }); } /** @@ -87,8 +93,8 @@ export class BreakageReportingSpec { * @param {Record} params.config * @return {Promise} */ - async setup (params) { - const { config } = params + async setup(params) { + const { config } = params; // read the built file from disk and do replacements const injectedJS = wrapWindowsScripts(this.build.artifact, { @@ -96,21 +102,21 @@ export class BreakageReportingSpec { $USER_UNPROTECTED_DOMAINS$: [], $USER_PREFERENCES$: { platform: { name: 'windows' }, - debug: true - } - }) + debug: true, + }, + }); await this.page.addInitScript(mockWindowsMessaging, { messagingContext: { env: 'development', context: 'contentScopeScripts', - featureName: 'n/a' + featureName: 'n/a', }, - responses: {} - }) + responses: {}, + }); // attach the JS - await this.page.addInitScript(injectedJS) + await this.page.addInitScript(injectedJS); } /** @@ -118,9 +124,9 @@ export class BreakageReportingSpec { * @param {import("@playwright/test").Page} page * @param {import("@playwright/test").TestInfo} testInfo */ - static create (page, testInfo) { + static create(page, testInfo) { // Read the configuration object to determine which platform we're testing against - const { platformInfo, build } = perPlatform(testInfo.project.use) - return new BreakageReportingSpec(page, build, platformInfo) + const { platformInfo, build } = perPlatform(testInfo.project.use); + return new BreakageReportingSpec(page, build, platformInfo); } } diff --git a/injected/integration-test/broker-protection.spec.js b/injected/integration-test/broker-protection.spec.js index 293c709ce..ee5ef353d 100644 --- a/injected/integration-test/broker-protection.spec.js +++ b/injected/integration-test/broker-protection.spec.js @@ -1,472 +1,456 @@ -import { test, expect } from '@playwright/test' -import { BrokerProtectionPage } from './page-objects/broker-protection.js' +import { test, expect } from '@playwright/test'; +import { BrokerProtectionPage } from './page-objects/broker-protection.js'; test.describe('Broker Protection communications', () => { test('sends an error when the action is not found', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('form.html') - await dbp.receivesAction('action-not-found.json') - await dbp.waitForMessage('actionError') - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('form.html'); + await dbp.receivesAction('action-not-found.json'); + await dbp.waitForMessage('actionError'); + }); test.describe('Executes invalid action and sends error message', () => { test('click element not on page', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('empty-form.html') - await dbp.receivesAction('click.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isErrorMessage(response) - }) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('empty-form.html'); + await dbp.receivesAction('click.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isErrorMessage(response); + }); + }); test.describe('Profile extraction', () => { test('extract', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results.html') - await dbp.receivesAction('extract.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'John Smith', - alternativeNames: [], - age: '38', - addresses: [ - { city: 'Chicago', state: 'IL' }, - { city: 'Cadillac', state: 'MI' }, - { city: 'Ypsilanti', state: 'MI' } - ], - phoneNumbers: [], - relatives: [ - 'Cheryl Lamar' - ], - profileUrl: baseURL + 'view/John-Smith-CyFdD.F', - identifier: baseURL + 'view/John-Smith-CyFdD.F' - }]) - dbp.responseContainsMetadata(response[0].payload.params.result.success.meta) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results.html'); + await dbp.receivesAction('extract.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'John Smith', + alternativeNames: [], + age: '38', + addresses: [ + { city: 'Chicago', state: 'IL' }, + { city: 'Cadillac', state: 'MI' }, + { city: 'Ypsilanti', state: 'MI' }, + ], + phoneNumbers: [], + relatives: ['Cheryl Lamar'], + profileUrl: baseURL + 'view/John-Smith-CyFdD.F', + identifier: baseURL + 'view/John-Smith-CyFdD.F', + }, + ]); + dbp.responseContainsMetadata(response[0].payload.params.result.success.meta); + }); test('extract with retry', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results.html?delay=2000') - await dbp.receivesAction('extract.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'John Smith', - alternativeNames: [], - age: '38', - addresses: [ - { city: 'Chicago', state: 'IL' }, - { city: 'Cadillac', state: 'MI' }, - { city: 'Ypsilanti', state: 'MI' } - ], - phoneNumbers: [], - relatives: [ - 'Cheryl Lamar' - ], - profileUrl: baseURL + 'view/John-Smith-CyFdD.F', - identifier: baseURL + 'view/John-Smith-CyFdD.F' - }]) - dbp.responseContainsMetadata(response[0].payload.params.result.success.meta) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results.html?delay=2000'); + await dbp.receivesAction('extract.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'John Smith', + alternativeNames: [], + age: '38', + addresses: [ + { city: 'Chicago', state: 'IL' }, + { city: 'Cadillac', state: 'MI' }, + { city: 'Ypsilanti', state: 'MI' }, + ], + phoneNumbers: [], + relatives: ['Cheryl Lamar'], + profileUrl: baseURL + 'view/John-Smith-CyFdD.F', + identifier: baseURL + 'view/John-Smith-CyFdD.F', + }, + ]); + dbp.responseContainsMetadata(response[0].payload.params.result.success.meta); + }); test('extract multiple profiles', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-multiple.html') - await dbp.receivesAction('extract2.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-multiple.html'); + await dbp.receivesAction('extract2.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); dbp.isExtractMatch(response[0].payload.params.result.success.response, [ { name: 'Ben Smith', - alternativeNames: [ - 'Ben S Smith' - ], + alternativeNames: ['Ben S Smith'], age: '40', addresses: [ { city: 'Miami', state: 'FL' }, { city: 'Miami Gardens', state: 'FL' }, - { city: 'Opa Locka', state: 'FL' } + { city: 'Opa Locka', state: 'FL' }, ], phoneNumbers: [], relatives: [], profileUrl: baseURL + 'view/Ben-Smith-CQEmF3CB', - identifier: baseURL + 'view/Ben-Smith-CQEmF3CB' + identifier: baseURL + 'view/Ben-Smith-CQEmF3CB', }, { name: 'Ben Smith', alternativeNames: [], age: '40', - addresses: [ - { city: 'Miami', state: 'FL' } - ], + addresses: [{ city: 'Miami', state: 'FL' }], phoneNumbers: [], relatives: [], profileUrl: baseURL + 'view/Ben-Smith-DSAJBtFB', - identifier: baseURL + 'view/Ben-Smith-DSAJBtFB' + identifier: baseURL + 'view/Ben-Smith-DSAJBtFB', }, { name: 'Benjamin H Smith', - alternativeNames: [ - 'Bejamin Smith', - 'Ben Smith', - 'Benjamin Smith' - ], + alternativeNames: ['Bejamin Smith', 'Ben Smith', 'Benjamin Smith'], age: '39', addresses: [ { city: 'Fort Lauderdale', - state: 'FL' + state: 'FL', }, { city: 'Miami', - state: 'FL' + state: 'FL', }, { city: 'Indianapolis', - state: 'IN' - } + state: 'IN', + }, ], phoneNumbers: [], relatives: [], profileUrl: baseURL + 'view/Benjamin-Smith-GpC.DQCB', - identifier: baseURL + 'view/Benjamin-Smith-GpC.DQCB' - } - ]) - }) + identifier: baseURL + 'view/Benjamin-Smith-GpC.DQCB', + }, + ]); + }); test('extract profiles test 3', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-alt.html') - await dbp.receivesAction('extract3.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'John A Smith', - age: '63', - alternativeNames: [ - 'John Smithe', - 'Jonathan Smith' - ], - addresses: [ - { city: 'Miami', state: 'FL' }, - { city: 'Orlando', state: 'FL' }, - { city: 'Plantation', state: 'FL' } - ], - profileUrl: baseURL + 'products/name?firstName=john&middleName=a&lastName=smith&ln=smith&city=orlando&state=fl&id=G421681744450237260', - identifier: baseURL + 'products/name?firstName=john&middleName=a&lastName=smith&ln=smith&city=orlando&state=fl&id=G421681744450237260', - phoneNumbers: [], - relatives: [] - }]) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-alt.html'); + await dbp.receivesAction('extract3.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'John A Smith', + age: '63', + alternativeNames: ['John Smithe', 'Jonathan Smith'], + addresses: [ + { city: 'Miami', state: 'FL' }, + { city: 'Orlando', state: 'FL' }, + { city: 'Plantation', state: 'FL' }, + ], + profileUrl: + baseURL + + 'products/name?firstName=john&middleName=a&lastName=smith&ln=smith&city=orlando&state=fl&id=G421681744450237260', + identifier: + baseURL + + 'products/name?firstName=john&middleName=a&lastName=smith&ln=smith&city=orlando&state=fl&id=G421681744450237260', + phoneNumbers: [], + relatives: [], + }, + ]); + }); test('extract profiles test 4', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-4.html') - await dbp.receivesAction('extract4.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'Ben Smith', - age: '55', - alternativeNames: [], - addresses: [ - { city: 'Tampa', state: 'FL' } - ], - profileUrl: baseURL + 'products/name?firstName=ben&lastName=smith&ln=smith&city=tampa&state=fl&id=G-3492284932683347509', - identifier: baseURL + 'products/name?firstName=ben&lastName=smith&ln=smith&city=tampa&state=fl&id=G-3492284932683347509', - phoneNumbers: [], - relatives: [] - }]) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-4.html'); + await dbp.receivesAction('extract4.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'Ben Smith', + age: '55', + alternativeNames: [], + addresses: [{ city: 'Tampa', state: 'FL' }], + profileUrl: + baseURL + 'products/name?firstName=ben&lastName=smith&ln=smith&city=tampa&state=fl&id=G-3492284932683347509', + identifier: + baseURL + 'products/name?firstName=ben&lastName=smith&ln=smith&city=tampa&state=fl&id=G-3492284932683347509', + phoneNumbers: [], + relatives: [], + }, + ]); + }); test('extract profiles test 5', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-5.html') - await dbp.receivesAction('extract5.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'Jonathan Smith', - age: '50', - alternativeNames: [], - phoneNumbers: [ - '97021405106' - ], - profileUrl: baseURL + 'person/Smith-41043103849', - identifier: baseURL + 'person/Smith-41043103849', - addresses: [ - { - city: 'Orlando', - state: 'FL' - } - ], - relatives: [] - }]) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-5.html'); + await dbp.receivesAction('extract5.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'Jonathan Smith', + age: '50', + alternativeNames: [], + phoneNumbers: ['97021405106'], + profileUrl: baseURL + 'person/Smith-41043103849', + identifier: baseURL + 'person/Smith-41043103849', + addresses: [ + { + city: 'Orlando', + state: 'FL', + }, + ], + relatives: [], + }, + ]); + }); test('extract profile from irregular HTML 1', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-irregular1.html') - await dbp.receivesAction('extract-irregular1.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'John M Smith', - age: '75', - alternativeNames: [ - 'John Ark', - 'John Mark', - 'John Smith', - 'John-Mark Smith', - 'Johna Smith', - 'Johnmark Smith' - ], - addresses: [ - { city: 'Chicago', state: 'IL' }, - { city: 'Evanston', state: 'IL' } - ], - profileUrl: baseURL + 'pp/John-Smith-HdDWHRBD', - identifier: baseURL + 'pp/John-Smith-HdDWHRBD', - relatives: [ - 'Margaret Kelly', - 'Mary Kelly', - 'Michael Kelly' - ], - phoneNumbers: [] - }]) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-irregular1.html'); + await dbp.receivesAction('extract-irregular1.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'John M Smith', + age: '75', + alternativeNames: ['John Ark', 'John Mark', 'John Smith', 'John-Mark Smith', 'Johna Smith', 'Johnmark Smith'], + addresses: [ + { city: 'Chicago', state: 'IL' }, + { city: 'Evanston', state: 'IL' }, + ], + profileUrl: baseURL + 'pp/John-Smith-HdDWHRBD', + identifier: baseURL + 'pp/John-Smith-HdDWHRBD', + relatives: ['Margaret Kelly', 'Mary Kelly', 'Michael Kelly'], + phoneNumbers: [], + }, + ]); + }); test('extract profile from irregular HTML 2', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-irregular2.html') - await dbp.receivesAction('extract-irregular2.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'John Smith', - age: '71', - addresses: [ - { city: 'Chicago', state: 'IL' }, - { city: 'South Holland', state: 'IL' }, - { city: 'Crown Point', state: 'IN' } - ], - alternativeNames: [], - relatives: [ - 'Brittany J Hoard', - 'Jame...', - 'Joyce E Doyle' - ], - profileUrl: baseURL + 'find/person/p286nuu00u98lu9n0n96', - identifier: baseURL + 'find/person/p286nuu00u98lu9n0n96', - phoneNumbers: [] - }]) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-irregular2.html'); + await dbp.receivesAction('extract-irregular2.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'John Smith', + age: '71', + addresses: [ + { city: 'Chicago', state: 'IL' }, + { city: 'South Holland', state: 'IL' }, + { city: 'Crown Point', state: 'IN' }, + ], + alternativeNames: [], + relatives: ['Brittany J Hoard', 'Jame...', 'Joyce E Doyle'], + profileUrl: baseURL + 'find/person/p286nuu00u98lu9n0n96', + identifier: baseURL + 'find/person/p286nuu00u98lu9n0n96', + phoneNumbers: [], + }, + ]); + }); test('extract profile from irregular HTML 3', async ({ page, baseURL }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-irregular3.html') - await dbp.receivesAction('extract-irregular3.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'John I Smith', - age: '59', - addresses: [ - { city: 'Chicago', state: 'IL' }, - { city: 'Forest Park', state: 'IL' }, - { city: 'Oak Park', state: 'IL' }, - { city: 'River Forest', state: 'IL' } - ], - alternativeNames: [ - 'John Farmersmith', - 'John Smith', - 'Johni Smith' - ], - phoneNumbers: [], - relatives: [ - 'Alexander Makely', - 'Ethel Makely', - 'Veronica Berrios' - ], - profileUrl: baseURL + 'people/John-Smith-AIGwGOFD', - identifier: baseURL + 'people/John-Smith-AIGwGOFD' - }]) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-irregular3.html'); + await dbp.receivesAction('extract-irregular3.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'John I Smith', + age: '59', + addresses: [ + { city: 'Chicago', state: 'IL' }, + { city: 'Forest Park', state: 'IL' }, + { city: 'Oak Park', state: 'IL' }, + { city: 'River Forest', state: 'IL' }, + ], + alternativeNames: ['John Farmersmith', 'John Smith', 'Johni Smith'], + phoneNumbers: [], + relatives: ['Alexander Makely', 'Ethel Makely', 'Veronica Berrios'], + profileUrl: baseURL + 'people/John-Smith-AIGwGOFD', + identifier: baseURL + 'people/John-Smith-AIGwGOFD', + }, + ]); + }); test('extracts profile and generates id', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results.html') - await dbp.receivesAction('extract-generate-id.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, [{ - name: 'John Smith', - alternativeNames: [], - age: '38', - addresses: [ - { city: 'Chicago', state: 'IL' }, - { city: 'Cadillac', state: 'MI' }, - { city: 'Ypsilanti', state: 'MI' } - ], - phoneNumbers: [], - relatives: [ - 'Cheryl Lamar' - ], - identifier: 'b3ccf90a0ffaaa5f57fd262ab1b694b3c208d622' - }]) - dbp.responseContainsMetadata(response[0].payload.params.result.success.meta) - }) - - test('returns an empty array when no profile selector matches but the no results selector is present', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-not-found.html') - await dbp.receivesAction('results-not-found-valid.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isExtractMatch(response[0].payload.params.result.success.response, []) - }) - - test('returns an error when no profile selector matches and the no results selector is not present', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results.html') - await dbp.receivesAction('results-not-found-invalid.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isErrorMessage(response) - }) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results.html'); + await dbp.receivesAction('extract-generate-id.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, [ + { + name: 'John Smith', + alternativeNames: [], + age: '38', + addresses: [ + { city: 'Chicago', state: 'IL' }, + { city: 'Cadillac', state: 'MI' }, + { city: 'Ypsilanti', state: 'MI' }, + ], + phoneNumbers: [], + relatives: ['Cheryl Lamar'], + identifier: 'b3ccf90a0ffaaa5f57fd262ab1b694b3c208d622', + }, + ]); + dbp.responseContainsMetadata(response[0].payload.params.result.success.meta); + }); + + test('returns an empty array when no profile selector matches but the no results selector is present', async ({ + page, + }, workerInfo) => { + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-not-found.html'); + await dbp.receivesAction('results-not-found-valid.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isExtractMatch(response[0].payload.params.result.success.response, []); + }); + + test('returns an error when no profile selector matches and the no results selector is not present', async ({ + page, + }, workerInfo) => { + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results.html'); + await dbp.receivesAction('results-not-found-invalid.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isErrorMessage(response); + }); + }); test.describe('Executes action and sends success message', () => { test('buildUrl', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results.html') - await dbp.receivesAction('navigate.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isUrlMatch(response[0].payload.params.result.success.response) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results.html'); + await dbp.receivesAction('navigate.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isUrlMatch(response[0].payload.params.result.success.response); + }); test('fillForm', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('form.html') - await dbp.receivesAction('fill-form.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - await dbp.isFormFilled() - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('form.html'); + await dbp.receivesAction('fill-form.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + await dbp.isFormFilled(); + }); test('click', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('form.html') - await dbp.receivesAction('click.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('form.html'); + await dbp.receivesAction('click.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + }); test('clicking with parent selector (considering matching weight/score)', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-weighted.html') - await dbp.receivesAction('click-weighted.json') - const response = await dbp.waitForMessage('actionCompleted') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-weighted.html'); + await dbp.receivesAction('click-weighted.json'); + const response = await dbp.waitForMessage('actionCompleted'); - dbp.isSuccessMessage(response) - await page.waitForURL(url => url.hash === '#2', { timeout: 2000 }) - }) + dbp.isSuccessMessage(response); + await page.waitForURL((url) => url.hash === '#2', { timeout: 2000 }); + }); test('clicking with parent selector (clicking the actual parent)', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('results-parent.html') - await dbp.receivesAction('click-parent.json') - const response = await dbp.waitForMessage('actionCompleted') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('results-parent.html'); + await dbp.receivesAction('click-parent.json'); + const response = await dbp.waitForMessage('actionCompleted'); - dbp.isSuccessMessage(response) - await page.waitForURL(url => url.hash === '#2', { timeout: 2000 }) - }) + dbp.isSuccessMessage(response); + await page.waitForURL((url) => url.hash === '#2', { timeout: 2000 }); + }); test('click multiple targets', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('click-multiple.html') - await dbp.receivesAction('click-multiple.json') - const response = await dbp.waitForMessage('actionCompleted') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('click-multiple.html'); + await dbp.receivesAction('click-multiple.json'); + const response = await dbp.waitForMessage('actionCompleted'); - dbp.isSuccessMessage(response) - await page.waitForURL(url => url.hash === '#1-2', { timeout: 2000 }) - }) + dbp.isSuccessMessage(response); + await page.waitForURL((url) => url.hash === '#1-2', { timeout: 2000 }); + }); test('getCaptchaInfo', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('captcha.html') - await dbp.receivesAction('get-captcha.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isCaptchaMatch(response[0].payload?.params.result.success.response) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('captcha.html'); + await dbp.receivesAction('get-captcha.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isCaptchaMatch(response[0].payload?.params.result.success.response); + }); test('getCaptchaInfo (hcaptcha)', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('captcha2.html') - await dbp.receivesAction('get-captcha.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isHCaptchaMatch(response[0].payload?.params.result.success.response) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('captcha2.html'); + await dbp.receivesAction('get-captcha.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isHCaptchaMatch(response[0].payload?.params.result.success.response); + }); test('remove query params from captcha url', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('captcha.html?fname=john&lname=smith') - await dbp.receivesAction('get-captcha.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - dbp.isQueryParamRemoved(response[0].payload?.params.result.success.response) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('captcha.html?fname=john&lname=smith'); + await dbp.receivesAction('get-captcha.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + dbp.isQueryParamRemoved(response[0].payload?.params.result.success.response); + }); test('solveCaptcha', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('captcha.html') - await dbp.receivesAction('solve-captcha.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - await dbp.isCaptchaTokenFilled() - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('captcha.html'); + await dbp.receivesAction('solve-captcha.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + await dbp.isCaptchaTokenFilled(); + }); test('expectation', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('form.html') - await dbp.receivesAction('expectation.json') - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - }) + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('form.html'); + await dbp.receivesAction('expectation.json'); + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + }); test('expectation: element exists', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('form.html') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('form.html'); // control: ensure the element is absent - await dbp.elementIsAbsent('.slow-element') + await dbp.elementIsAbsent('.slow-element'); // now send in the action await dbp.receivesInlineAction({ @@ -478,73 +462,73 @@ test.describe('Broker Protection communications', () => { { type: 'element', selector: '.slow-element', - parent: 'body.delay-complete' - } - ] - } - } - }) - - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - }) - }) + parent: 'body.delay-complete', + }, + ], + }, + }, + }); + + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + }); + }); test('expectation with actions', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('expectation-actions.html') - await dbp.receivesAction('expectation-actions.json') - const response = await dbp.waitForMessage('actionCompleted') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('expectation-actions.html'); + await dbp.receivesAction('expectation-actions.json'); + const response = await dbp.waitForMessage('actionCompleted'); - dbp.isSuccessMessage(response) - await page.waitForURL(url => url.hash === '#1', { timeout: 2000 }) - }) + dbp.isSuccessMessage(response); + await page.waitForURL((url) => url.hash === '#1', { timeout: 2000 }); + }); test('expectation fails when failSilently is not present', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('expectation-actions.html') - await dbp.receivesAction('expectation-actions-fail.json') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('expectation-actions.html'); + await dbp.receivesAction('expectation-actions-fail.json'); - const response = await dbp.waitForMessage('actionCompleted') - dbp.isErrorMessage(response) + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isErrorMessage(response); - const currentUrl = page.url() - expect(currentUrl).not.toContain('#') - }) + const currentUrl = page.url(); + expect(currentUrl).not.toContain('#'); + }); test('expectation succeeds when failSilently is present', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('expectation-actions.html') - await dbp.receivesAction('expectation-actions-fail-silently.json') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('expectation-actions.html'); + await dbp.receivesAction('expectation-actions-fail-silently.json'); - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); - const currentUrl = page.url() - expect(currentUrl).not.toContain('#') - }) + const currentUrl = page.url(); + expect(currentUrl).not.toContain('#'); + }); test('expectation succeeds but subaction fails should throw error', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('expectation-actions.html') - await dbp.receivesAction('expectation-actions-subaction-fail.json') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('expectation-actions.html'); + await dbp.receivesAction('expectation-actions-subaction-fail.json'); - const response = await dbp.waitForMessage('actionCompleted') - dbp.isErrorMessage(response) + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isErrorMessage(response); - const currentUrl = page.url() - expect(currentUrl).not.toContain('#') - }) + const currentUrl = page.url(); + expect(currentUrl).not.toContain('#'); + }); test.describe('retrying', () => { test('retrying a click', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('retry.html') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('retry.html'); await dbp.simulateSubscriptionMessage('onActionReceived', { state: { @@ -554,26 +538,26 @@ test.describe('Broker Protection communications', () => { retry: { environment: 'web', maxAttempts: 10, - interval: { ms: 1000 } + interval: { ms: 1000 }, }, elements: [ { type: 'button', - selector: 'button' - } - ] - } - } - }) - await page.getByRole('heading', { name: 'Retry' }).waitFor({ timeout: 5000 }) - - const response = await dbp.waitForMessage('actionCompleted') - dbp.isSuccessMessage(response) - }) + selector: 'button', + }, + ], + }, + }, + }); + await page.getByRole('heading', { name: 'Retry' }).waitFor({ timeout: 5000 }); + + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + }); test('ensuring retry doesnt apply everywhere', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo) - await dbp.enabled() - await dbp.navigatesTo('retry.html') + const dbp = BrokerProtectionPage.create(page, workerInfo); + await dbp.enabled(); + await dbp.navigatesTo('retry.html'); await dbp.simulateSubscriptionMessage('onActionReceived', { state: { @@ -583,15 +567,15 @@ test.describe('Broker Protection communications', () => { elements: [ { type: 'button', - selector: 'button' - } - ] - } - } - }) - - const response = await dbp.waitForMessage('actionCompleted') - dbp.isErrorMessage(response) - }) - }) -}) + selector: 'button', + }, + ], + }, + }, + }); + + const response = await dbp.waitForMessage('actionCompleted'); + dbp.isErrorMessage(response); + }); + }); +}); diff --git a/injected/integration-test/cookie.spec.js b/injected/integration-test/cookie.spec.js index 32dd90ccf..11ed9bed1 100644 --- a/injected/integration-test/cookie.spec.js +++ b/injected/integration-test/cookie.spec.js @@ -1,65 +1,65 @@ -import { test as base, expect } from '@playwright/test' -import { gotoAndWait, testContextForExtension } from './helpers/harness.js' +import { test as base, expect } from '@playwright/test'; +import { gotoAndWait, testContextForExtension } from './helpers/harness.js'; -const test = testContextForExtension(base) +const test = testContextForExtension(base); test.describe('Cookie protection tests', () => { test('should restrict the expiry of first-party cookies', async ({ page }) => { - await gotoAndWait(page, '/index.html') + await gotoAndWait(page, '/index.html'); const result = await page.evaluate(async () => { - document.cookie = 'test=1; expires=Wed, 21 Aug 2040 20:00:00 UTC;' + document.cookie = 'test=1; expires=Wed, 21 Aug 2040 20:00:00 UTC;'; // wait for a tick, as cookie modification happens in a promise - await new Promise((resolve) => setTimeout(resolve, 1)) + await new Promise((resolve) => setTimeout(resolve, 1)); // @ts-expect-error - cookieStore API types are missing here // eslint-disable-next-line no-undef - return cookieStore.get('test') - }) - expect(result.name).toEqual('test') - expect(result.value).toEqual('1') - expect(result.expires).toBeLessThan(Date.now() + 605_000_000) - }) + return cookieStore.get('test'); + }); + expect(result.name).toEqual('test'); + expect(result.value).toEqual('1'); + expect(result.expires).toBeLessThan(Date.now() + 605_000_000); + }); test('non-string cookie values do not bypass protection', async ({ page }) => { - await gotoAndWait(page, '/index.html') + await gotoAndWait(page, '/index.html'); const result = await page.evaluate(async () => { // @ts-expect-error - Invalid argument to document.cookie on purpose for test document.cookie = { - toString () { - const expires = (new Date(+new Date() + 86400 * 1000 * 100)).toUTCString() - return 'a=b; expires=' + expires - } - } + toString() { + const expires = new Date(+new Date() + 86400 * 1000 * 100).toUTCString(); + return 'a=b; expires=' + expires; + }, + }; // wait for a tick, as cookie modification happens in a promise - await new Promise((resolve) => setTimeout(resolve, 1)) + await new Promise((resolve) => setTimeout(resolve, 1)); // @ts-expect-error - cookieStore API types are missing here // eslint-disable-next-line no-undef - return cookieStore.get('a') - }) - expect(result.name).toEqual('a') - expect(result.value).toEqual('b') - expect(result.expires).toBeLessThan(Date.now() + 605_000_000) - }) + return cookieStore.get('a'); + }); + expect(result.name).toEqual('a'); + expect(result.value).toEqual('b'); + expect(result.expires).toBeLessThan(Date.now() + 605_000_000); + }); test('Erroneous values do not throw', async ({ page }) => { - await gotoAndWait(page, '/index.html') + await gotoAndWait(page, '/index.html'); const result = await page.evaluate(async () => { - document.cookie = 'a=b; expires=Wed, 21 Aug 2040 20:00:00 UTC;' + document.cookie = 'a=b; expires=Wed, 21 Aug 2040 20:00:00 UTC;'; // @ts-expect-error - Invalid argument to document.cookie on purpose for test - document.cookie = null + document.cookie = null; // @ts-expect-error - Invalid argument to document.cookie on purpose for test - document.cookie = undefined + document.cookie = undefined; // wait for a tick, as cookie modification happens in a promise - await new Promise((resolve) => setTimeout(resolve, 1)) + await new Promise((resolve) => setTimeout(resolve, 1)); // @ts-expect-error - cookieStore API types are missing here // eslint-disable-next-line no-undef - return cookieStore.get('a') - }) - expect(result.name).toEqual('a') - expect(result.value).toEqual('b') - expect(result.expires).toBeLessThan(Date.now() + 605_000_000) - }) -}) + return cookieStore.get('a'); + }); + expect(result.name).toEqual('a'); + expect(result.value).toEqual('b'); + expect(result.expires).toBeLessThan(Date.now() + 605_000_000); + }); +}); diff --git a/injected/integration-test/duckplayer-mobile.spec.js b/injected/integration-test/duckplayer-mobile.spec.js index 8ab5d002d..375db575c 100644 --- a/injected/integration-test/duckplayer-mobile.spec.js +++ b/injected/integration-test/duckplayer-mobile.spec.js @@ -1,123 +1,149 @@ -import { expect, test } from '@playwright/test' -import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js' +import { expect, test } from '@playwright/test'; +import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js'; test.describe('Video Player overlays', () => { - test('Selecting \'watch here\' on mobile', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + test("Selecting 'watch here' on mobile", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); // watch here = overlays removed - await overlays.mobile.choosesWatchHere() - await overlays.mobile.overlayIsRemoved() + await overlays.mobile.choosesWatchHere(); + await overlays.mobile.overlayIsRemoved(); await overlays.pixels.sendsPixels([ { pixelName: 'overlay', params: {} }, - { pixelName: 'play.do_not_use', params: { remember: '0' } } - ]) - }) - test('Selecting \'watch here\' on mobile + remember', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + { pixelName: 'play.do_not_use', params: { remember: '0' } }, + ]); + }); + test("Selecting 'watch here' on mobile + remember", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); // watch here = overlays removed - await overlays.mobile.selectsRemember() - await overlays.mobile.choosesWatchHere() - await overlays.mobile.overlayIsRemoved() + await overlays.mobile.selectsRemember(); + await overlays.mobile.choosesWatchHere(); + await overlays.mobile.overlayIsRemoved(); await overlays.pixels.sendsPixels([ { pixelName: 'overlay', params: {} }, - { pixelName: 'play.do_not_use', params: { remember: '1' } } - ]) - await overlays.userSettingWasUpdatedTo('disabled') - }) - test('Selecting \'watch in duckplayer\' on mobile', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + { pixelName: 'play.do_not_use', params: { remember: '1' } }, + ]); + await overlays.userSettingWasUpdatedTo('disabled'); + }); + test("Selecting 'watch in duckplayer' on mobile", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); - await overlays.mobile.choosesDuckPlayer() + await overlays.mobile.choosesDuckPlayer(); await overlays.pixels.sendsPixels([ { pixelName: 'overlay', params: {} }, - { pixelName: 'play.use', params: { remember: '0' } } - ]) - await overlays.userSettingWasUpdatedTo('always ask') - }) - test('Selecting \'watch in duckplayer\' on mobile + remember', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + { pixelName: 'play.use', params: { remember: '0' } }, + ]); + await overlays.userSettingWasUpdatedTo('always ask'); + }); + test("Selecting 'watch in duckplayer' on mobile + remember", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); - await overlays.mobile.selectsRemember() - await overlays.mobile.choosesDuckPlayer() + await overlays.mobile.selectsRemember(); + await overlays.mobile.choosesDuckPlayer(); await overlays.pixels.sendsPixels([ { pixelName: 'overlay', params: {} }, - { pixelName: 'play.use', params: { remember: '1' } } - ]) - await overlays.userSettingWasUpdatedTo('enabled') - }) + { pixelName: 'play.use', params: { remember: '1' } }, + ]); + await overlays.userSettingWasUpdatedTo('enabled'); + }); test('opens info', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() - await overlays.mobile.opensInfo() - }) -}) + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); + await overlays.mobile.opensInfo(); + }); +}); /** * Use this test in `--headed` mode to cycle through every language */ test.describe.skip('Translated Overlays', () => { - const items = ['bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'et', 'fi', 'fr', 'hr', 'hu', 'it', 'lt', 'lv', 'nb', 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sv', 'tr'] + const items = [ + 'bg', + 'cs', + 'da', + 'de', + 'el', + 'en', + 'es', + 'et', + 'fi', + 'fr', + 'hr', + 'hu', + 'it', + 'lt', + 'lv', + 'nb', + 'nl', + 'pl', + 'pt', + 'ro', + 'ru', + 'sk', + 'sl', + 'sv', + 'tr', + ]; // const items = ['en'] for (const locale of items) { test(`testing UI ${locale}`, async ({ page }, workerInfo) => { // console.log(workerInfo.project.use.viewport.height) // console.log(workerInfo.project.use.viewport.width) - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ locale }) - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() - await page.locator('ddg-video-overlay-mobile').nth(0).waitFor() - await page.locator('.html5-video-player').screenshot({ path: `screens/se-2/${locale}.png` }) - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ locale }); + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); + await page.locator('ddg-video-overlay-mobile').nth(0).waitFor(); + await page.locator('.html5-video-player').screenshot({ path: `screens/se-2/${locale}.png` }); + }); } -}) +}); /** * Use `npm run playwright-screenshots` to run this test only. */ test.describe('Overlay screenshot @screenshots', () => { - test('testing Overlay UI \'en\'', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ locale: 'en' }) - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() - await page.locator('ddg-video-overlay-mobile').nth(0).waitFor() - await expect(page.locator('.html5-video-player')).toHaveScreenshot('overlay.png', { maxDiffPixels: 20 }) - }) -}) + test("testing Overlay UI 'en'", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ locale: 'en' }); + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); + await page.locator('ddg-video-overlay-mobile').nth(0).waitFor(); + await expect(page.locator('.html5-video-player')).toHaveScreenshot('overlay.png', { maxDiffPixels: 20 }); + }); +}); diff --git a/injected/integration-test/duckplayer-remote-config.spec.js b/injected/integration-test/duckplayer-remote-config.spec.js index 8d8cce57b..044d69fd9 100644 --- a/injected/integration-test/duckplayer-remote-config.spec.js +++ b/injected/integration-test/duckplayer-remote-config.spec.js @@ -1,75 +1,75 @@ -import { test } from '@playwright/test' -import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js' +import { test } from '@playwright/test'; +import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js'; test.describe('Remote config', () => { test('feature: disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'disabled.json' }) - await overlays.gotoThumbsPage() - await overlays.overlaysDontShow() - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'disabled.json' }); + await overlays.gotoThumbsPage(); + await overlays.overlaysDontShow(); + }); test('thumbnailOverlays: disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'thumbnail-overlays-disabled.json' }) - await overlays.gotoThumbsPage() - await overlays.overlaysDontShow() - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'thumbnail-overlays-disabled.json' }); + await overlays.gotoThumbsPage(); + await overlays.overlaysDontShow(); + }); test('clickInterception: disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'click-interceptions-disabled.json' }) - await overlays.userSettingIs('enabled') - await overlays.gotoThumbsPage() - const navigation = overlays.requestWillFail() - await overlays.clicksFirstThumbnail() - await navigation - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'click-interceptions-disabled.json' }); + await overlays.userSettingIs('enabled'); + await overlays.gotoThumbsPage(); + const navigation = overlays.requestWillFail(); + await overlays.clicksFirstThumbnail(); + await navigation; + }); test('videoOverlays: disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'video-overlays-disabled.json' }) - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() - await page.waitForTimeout(1000) // <-- We need to prove the overlay doesn't show. - await overlays.videoOverlayDoesntShow() - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'video-overlays-disabled.json' }); + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); + await page.waitForTimeout(1000); // <-- We need to prove the overlay doesn't show. + await overlays.videoOverlayDoesntShow(); + }); test('excludedRegions: CSS selectors on video page', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays.json' }) - await overlays.gotoPlayerPage() - await overlays.overlayBlocksVideo() - await overlays.hoverAThumbnailInExcludedRegion('#playlist') - await overlays.overlaysDontShow() - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays.json' }); + await overlays.gotoPlayerPage(); + await overlays.overlayBlocksVideo(); + await overlays.hoverAThumbnailInExcludedRegion('#playlist'); + await overlays.overlaysDontShow(); + }); test('hoverExcluded: CSS selectors to ignore hovers', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays.json' }) - await overlays.gotoThumbsPage({ variant: 'cookie_banner' }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays.json' }); + await overlays.gotoThumbsPage({ variant: 'cookie_banner' }); // this is covered with a known overlay - await overlays.hoverNthThumbnail(0) + await overlays.hoverNthThumbnail(0); // assert Dax doesn't show - await overlays.overlaysDontShow() + await overlays.overlaysDontShow(); // now hover a thumb without the overlay - await overlays.hoverNthThumbnail(3) + await overlays.hoverNthThumbnail(3); // overlay should be visible - await overlays.isVisible() - }) + await overlays.isVisible(); + }); test('clickExcluded: CSS selectors to ignore clicks', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays.json' }) - await overlays.userSettingIs('enabled') + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays.json' }); + await overlays.userSettingIs('enabled'); - await overlays.gotoThumbsPage({ variant: 'cookie_banner' }) + await overlays.gotoThumbsPage({ variant: 'cookie_banner' }); // control - ensure messages are being sent, to prevent false positives // this thumb does NOT have the overlay - await overlays.clickNthThumbnail(3) - await overlays.duckPlayerLoadsFor('4') // this is ID of the 4th element + await overlays.clickNthThumbnail(3); + await overlays.duckPlayerLoadsFor('4'); // this is ID of the 4th element // now click the first thumb, that's covered with an overlay - await overlays.clickNthThumbnail(0) - await overlays.duckPlayerLoadedTimes(1) // still 1, from the first call - }) -}) + await overlays.clickNthThumbnail(0); + await overlays.duckPlayerLoadedTimes(1); // still 1, from the first call + }); +}); diff --git a/injected/integration-test/duckplayer.e2e.spec.js b/injected/integration-test/duckplayer.e2e.spec.js index 7aa17eb3e..85d1a207e 100644 --- a/injected/integration-test/duckplayer.e2e.spec.js +++ b/injected/integration-test/duckplayer.e2e.spec.js @@ -1,102 +1,102 @@ -import { test } from '@playwright/test' -import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js' +import { test } from '@playwright/test'; +import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js'; test.describe('e2e: Duck Player Thumbnail Overlays on YouTube.com', () => { test('e2e: Overlays never appear on "shorts"', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.gotoYoutubeHomepage() + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.gotoYoutubeHomepage(); // Ensure the hover works normally to prevent false positives - await overlays.hoverAYouTubeThumbnail() - await overlays.isVisible() + await overlays.hoverAYouTubeThumbnail(); + await overlays.isVisible(); // now ensure the hover doesn't work on shorts - await overlays.hoverShort() - await overlays.overlaysDontShow() - }) + await overlays.hoverShort(); + await overlays.overlaysDontShow(); + }); test('e2e: Overlays appear on "search pages"', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.gotoYoutubeSearchPageForMovie() - await overlays.hoverAYouTubeThumbnail() - await overlays.isVisible() + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.gotoYoutubeSearchPageForMovie(); + await overlays.hoverAYouTubeThumbnail(); + await overlays.isVisible(); - await overlays.hoverAMovieThumb() - await overlays.overlaysDontShow() - }) + await overlays.hoverAMovieThumb(); + await overlays.overlaysDontShow(); + }); test('control (without our script): clicking on a short loads correctly', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.gotoYoutubeHomepage() - await page.waitForTimeout(2000) - await overlays.clicksFirstShortsThumbnail() - await overlays.showsShortsPage() - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.gotoYoutubeHomepage(); + await page.waitForTimeout(2000); + await overlays.clicksFirstShortsThumbnail(); + await overlays.showsShortsPage(); + }); test.describe('when enabled', () => { test('shorts do not intercept clicks', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.userSettingIs('enabled') - await overlays.gotoYoutubeHomepage() - await page.waitForTimeout(2000) - await overlays.clicksFirstShortsThumbnail() - await overlays.showsShortsPage() - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.userSettingIs('enabled'); + await overlays.gotoYoutubeHomepage(); + await page.waitForTimeout(2000); + await overlays.clicksFirstShortsThumbnail(); + await overlays.showsShortsPage(); + }); test('settings can change to disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.userSettingIs('enabled') - await overlays.gotoYoutubeHomepage() + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.userSettingIs('enabled'); + await overlays.gotoYoutubeHomepage(); await test.step('initial load, clicking a thumb -> duck player', async () => { - const videoId = await overlays.clicksFirstThumbnail() - await overlays.duckPlayerLoadsFor(videoId) - }) + const videoId = await overlays.clicksFirstThumbnail(); + await overlays.duckPlayerLoadsFor(videoId); + }); await test.step('settings updated to `disabled` -> YT', async () => { - await overlays.userChangedSettingTo('disabled') - const videoId = await overlays.clicksFirstThumbnail() - await overlays.showsVideoPageFor(videoId) - }) - }) - }) + await overlays.userChangedSettingTo('disabled'); + const videoId = await overlays.clicksFirstThumbnail(); + await overlays.showsVideoPageFor(videoId); + }); + }); + }); test.describe('when disabled', () => { test('disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.userSettingIs('disabled') - await overlays.gotoYoutubeHomepage() + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.userSettingIs('disabled'); + await overlays.gotoYoutubeHomepage(); await test.step('clicking first thumb -> video', async () => { - const videoId = await overlays.clicksFirstThumbnail() - await overlays.showsVideoPageFor(videoId) - }) + const videoId = await overlays.clicksFirstThumbnail(); + await overlays.showsVideoPageFor(videoId); + }); await test.step('click first thumb with setting enabled', async () => { - await overlays.gotoYoutubeHomepage() - await overlays.userChangedSettingTo('enabled') - const videoId = await overlays.clicksFirstThumbnail() - await overlays.duckPlayerLoadsFor(videoId) - }) - }) - }) + await overlays.gotoYoutubeHomepage(); + await overlays.userChangedSettingTo('enabled'); + const videoId = await overlays.clicksFirstThumbnail(); + await overlays.duckPlayerLoadsFor(videoId); + }); + }); + }); test.describe('e2e: video overlay', () => { test('setting: always ask', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.userSettingIs('always ask') - await overlays.gotoYoutubeVideo() - await overlays.overlayBlocksVideo() - }) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.userSettingIs('always ask'); + await overlays.gotoYoutubeVideo(); + await overlays.overlayBlocksVideo(); + }); test('setting: disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.userSettingIs('always ask') - await overlays.gotoYoutubeVideo() - await overlays.overlayBlocksVideo() - await overlays.userChangedSettingTo('enabled') - }) - }) -}) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.userSettingIs('always ask'); + await overlays.gotoYoutubeVideo(); + await overlays.overlayBlocksVideo(); + await overlays.userChangedSettingTo('enabled'); + }); + }); +}); diff --git a/injected/integration-test/duckplayer.setup.e2e.spec.js b/injected/integration-test/duckplayer.setup.e2e.spec.js index 47d040484..56c6bd6fd 100644 --- a/injected/integration-test/duckplayer.setup.e2e.spec.js +++ b/injected/integration-test/duckplayer.setup.e2e.spec.js @@ -1,15 +1,15 @@ -import { test } from '@playwright/test' -import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js' -import { STORAGE_STATE } from '../playwright-e2e.config.js' +import { test } from '@playwright/test'; +import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js'; +import { STORAGE_STATE } from '../playwright-e2e.config.js'; test.describe('e2e: Dismiss cookies', () => { test('storage locally', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); - await overlays.withRemoteConfig({ json: 'overlays-live.json' }) - await overlays.gotoYoutubeHomepage() - await overlays.dismissCookies() + await overlays.withRemoteConfig({ json: 'overlays-live.json' }); + await overlays.gotoYoutubeHomepage(); + await overlays.dismissCookies(); - await page.context().storageState({ path: STORAGE_STATE }) - }) -}) + await page.context().storageState({ path: STORAGE_STATE }); + }); +}); diff --git a/injected/integration-test/duckplayer.spec.js b/injected/integration-test/duckplayer.spec.js index 4dd019eb6..ca0aa628d 100644 --- a/injected/integration-test/duckplayer.spec.js +++ b/injected/integration-test/duckplayer.spec.js @@ -1,371 +1,371 @@ -import { test } from '@playwright/test' -import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js' +import { test } from '@playwright/test'; +import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js'; test.describe('Thumbnail Overlays', () => { test('Overlays show on thumbnails when enabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given the "overlays" feature is enabled - await overlays.withRemoteConfig() - await overlays.gotoThumbsPage() + await overlays.withRemoteConfig(); + await overlays.gotoThumbsPage(); // When I hover any video thumbnail - await overlays.hoverAThumbnail() + await overlays.hoverAThumbnail(); // Then our overlay shows - await overlays.isVisible() - }) + await overlays.isVisible(); + }); test('Overlays never appear on "shorts"', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig() - await overlays.gotoThumbsPage() + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig(); + await overlays.gotoThumbsPage(); // Ensure the hover works normally to prevent false positives - await overlays.hoverAThumbnail() - await overlays.isVisible() + await overlays.hoverAThumbnail(); + await overlays.isVisible(); // now ensure the hover doesn't work on shorts - await overlays.hoverShort() - await overlays.overlaysDontShow() - }) + await overlays.hoverShort(); + await overlays.overlaysDontShow(); + }); /** * https://app.asana.com/0/1201048563534612/1204993915251837/f */ test('Clicks are not intercepted on shorts when "enabled"', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.withRemoteConfig() - await overlays.userSettingIs('enabled') - await overlays.gotoThumbsPage() - const navigation = overlays.requestWillFail() - await overlays.clicksFirstShortsThumbnail() - const url = await navigation - overlays.opensShort(url) - }) - test('Overlays don\'t show on thumbnails when disabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.withRemoteConfig(); + await overlays.userSettingIs('enabled'); + await overlays.gotoThumbsPage(); + const navigation = overlays.requestWillFail(); + await overlays.clicksFirstShortsThumbnail(); + const url = await navigation; + overlays.opensShort(url); + }); + test("Overlays don't show on thumbnails when disabled", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given the "overlays" feature is disabled - await overlays.overlaysDisabled() - await overlays.gotoThumbsPage() + await overlays.overlaysDisabled(); + await overlays.gotoThumbsPage(); // Then our overlays never show - await overlays.overlaysDontShow() - }) + await overlays.overlaysDontShow(); + }); test('Overlays link to Duck Player', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given the "overlays" feature is enabled - await overlays.withRemoteConfig() - await overlays.gotoThumbsPage() + await overlays.withRemoteConfig(); + await overlays.gotoThumbsPage(); // When I click the DDG overlay - await overlays.clickDDGOverlay() + await overlays.clickDDGOverlay(); // Then our player loads for the correct video - await overlays.duckPlayerLoadsFor('1') - }) + await overlays.duckPlayerLoadsFor('1'); + }); test('Overlays dont show when user setting is "enabled"', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given the "overlays" feature is enabled - await overlays.withRemoteConfig() - await overlays.userSettingIs('enabled') - await overlays.gotoThumbsPage() - await overlays.overlaysDontShow() - }) + await overlays.withRemoteConfig(); + await overlays.userSettingIs('enabled'); + await overlays.gotoThumbsPage(); + await overlays.overlaysDontShow(); + }); test('Overlays dont show when user setting is "disabled"', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given the "overlays" feature is enabled - await overlays.withRemoteConfig() - await overlays.userSettingIs('disabled') - await overlays.gotoThumbsPage() - await overlays.overlaysDontShow() - }) + await overlays.withRemoteConfig(); + await overlays.userSettingIs('disabled'); + await overlays.gotoThumbsPage(); + await overlays.overlaysDontShow(); + }); test('Overlays appear when updated settings arrive', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given the "overlays" feature is enabled - await overlays.withRemoteConfig() - await overlays.userSettingIs('disabled') - await overlays.gotoThumbsPage() + await overlays.withRemoteConfig(); + await overlays.userSettingIs('disabled'); + await overlays.gotoThumbsPage(); // Nothing shown initially - await overlays.overlaysDontShow() + await overlays.overlaysDontShow(); // now receive an update - await overlays.userChangedSettingTo('always ask') + await overlays.userChangedSettingTo('always ask'); // overlays act as normal - await overlays.hoverAThumbnail() - await overlays.isVisible() - }) + await overlays.hoverAThumbnail(); + await overlays.isVisible(); + }); test('Overlays disappear when updated settings arrive', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given the "overlays" feature is enabled - await overlays.withRemoteConfig() - await overlays.userSettingIs('always ask') - await overlays.gotoThumbsPage() + await overlays.withRemoteConfig(); + await overlays.userSettingIs('always ask'); + await overlays.gotoThumbsPage(); // overlays act as normal initially - await overlays.hoverAThumbnail() - await overlays.isVisible() + await overlays.hoverAThumbnail(); + await overlays.isVisible(); // now receive an update - await overlays.userChangedSettingTo('disabled') + await overlays.userChangedSettingTo('disabled'); // overlays should be removed - await overlays.overlaysDontShow() - }) -}) + await overlays.overlaysDontShow(); + }); +}); test.describe('Video Player overlays', () => { test('Overlay blocks video from playing', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); // Then the overlay shows and blocks the video from playing - await overlays.overlayBlocksVideo() - }) + await overlays.overlayBlocksVideo(); + }); test('Overlay blocks video from playing (supporting DOM appearing over time)', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage({ variant: 'incremental-dom' }) + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage({ variant: 'incremental-dom' }); // Then the overlay shows and blocks the video from playing - await overlays.overlayBlocksVideo() - }) + await overlays.overlayBlocksVideo(); + }); test('Overlay is removed when new settings arrive', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); // then the overlay shows and blocks the video from playing - await overlays.overlayBlocksVideo() + await overlays.overlayBlocksVideo(); // When the user changes settings though - await overlays.userChangedSettingTo('disabled') + await overlays.userChangedSettingTo('disabled'); // No small overlays - await overlays.overlaysDontShow() + await overlays.overlaysDontShow(); // No video overlay - await overlays.videoOverlayDoesntShow() - }) + await overlays.videoOverlayDoesntShow(); + }); test('Overlay alters to suit new video id after navigation', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig({ json: 'overlays.json' }) + await overlays.withRemoteConfig({ json: 'overlays.json' }); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage({ videoID: '123456' }) + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage({ videoID: '123456' }); // then the overlay shows and blocks the video from playing - await overlays.overlayBlocksVideo() - await overlays.hasWatchLinkFor({ videoID: '123456' }) + await overlays.overlayBlocksVideo(); + await overlays.hasWatchLinkFor({ videoID: '123456' }); // now simulate going to another video from the related feed - await overlays.clickRelatedThumb({ videoID: 'abc1' }) + await overlays.clickRelatedThumb({ videoID: 'abc1' }); // should still be visible - await overlays.overlayBlocksVideo() + await overlays.overlayBlocksVideo(); // and watch link is updated - await overlays.hasWatchLinkFor({ videoID: 'abc1' }) - }) + await overlays.hasWatchLinkFor({ videoID: 'abc1' }); + }); test('Small overlay is displayed on video', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask remembered') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask remembered'); + await overlays.gotoPlayerPage(); // Then the overlay shows and blocks the video from playing - await overlays.smallOverlayShows() - }) - test('Small overlay is shown when setting is \'enabled\'', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + await overlays.smallOverlayShows(); + }); + test("Small overlay is shown when setting is 'enabled'", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('enabled') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('enabled'); + await overlays.gotoPlayerPage(); // Then the overlay shows and blocks the video from playing - await overlays.smallOverlayShows() - }) - test('Overlays are not shown when setting is \'disabled\'', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + await overlays.smallOverlayShows(); + }); + test("Overlays are not shown when setting is 'disabled'", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('disabled') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('disabled'); + await overlays.gotoPlayerPage(); // No small overlays - await overlays.overlaysDontShow() + await overlays.overlaysDontShow(); // No video overlay - await overlays.videoOverlayDoesntShow() - }) - test('Selecting \'Turn On Duck Player\'', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + await overlays.videoOverlayDoesntShow(); + }); + test("Selecting 'Turn On Duck Player'", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); - await overlays.turnOnDuckPlayer() - await overlays.userSettingWasUpdatedTo('always ask') // not updated - }) - test('Selecting \'Turn On Duck Player\' + remember', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + await overlays.turnOnDuckPlayer(); + await overlays.userSettingWasUpdatedTo('always ask'); // not updated + }); + test("Selecting 'Turn On Duck Player' + remember", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); - await overlays.rememberMyChoice() - await overlays.turnOnDuckPlayer() - await overlays.userSettingWasUpdatedTo('enabled') // updated - }) - test('Selecting \'No Thanks\'', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + await overlays.rememberMyChoice(); + await overlays.turnOnDuckPlayer(); + await overlays.userSettingWasUpdatedTo('enabled'); // updated + }); + test("Selecting 'No Thanks'", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); - await overlays.noThanks() - await overlays.secondOverlayExistsOnVideo() - }) - test('Selecting \'No Thanks\' + remember', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + await overlays.noThanks(); + await overlays.secondOverlayExistsOnVideo(); + }); + test("Selecting 'No Thanks' + remember", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage() + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage(); - await overlays.rememberMyChoice() - await overlays.noThanks() - await overlays.userSettingWasUpdatedTo('always ask remembered') // updated - }) + await overlays.rememberMyChoice(); + await overlays.noThanks(); + await overlays.userSettingWasUpdatedTo('always ask remembered'); // updated + }); test.describe('with remote config overrides', () => { - test('Selecting \'No Thanks\' + remember', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + test("Selecting 'No Thanks' + remember", async ({ page }, workerInfo) => { + const overlays = DuckplayerOverlays.create(page, workerInfo); // config with some CSS selectors overridden - await overlays.withRemoteConfig({ json: 'video-alt-selectors.json' }) + await overlays.withRemoteConfig({ json: 'video-alt-selectors.json' }); // And my setting is 'always ask' - await overlays.userSettingIs('always ask') - await overlays.gotoPlayerPage({ pageType: 'videoAltSelectors' }) - await overlays.overlayBlocksVideo() - }) - }) + await overlays.userSettingIs('always ask'); + await overlays.gotoPlayerPage({ pageType: 'videoAltSelectors' }); + await overlays.overlayBlocksVideo(); + }); + }); test.describe('with UI settings overrides', () => { test('displays default overlay copy', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' // And no overlay copy experiment cohort is set - await overlays.initialSetupIs('always ask') - await overlays.gotoPlayerPage() + await overlays.initialSetupIs('always ask'); + await overlays.gotoPlayerPage(); // Then the overlay shows the correct copy for the default variant - await overlays.overlayCopyIsDefault() - }) + await overlays.overlayCopyIsDefault(); + }); test('forces next video to play in Duck Player', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' // And the UI setting for 'play in Duck Player' is set to true - await overlays.initialSetupIs('always ask', 'play in duck player') - await overlays.gotoThumbsPage() - await overlays.overlaysDontShow() + await overlays.initialSetupIs('always ask', 'play in duck player'); + await overlays.gotoThumbsPage(); + await overlays.overlaysDontShow(); // When I click on the first thumbnail - await overlays.clicksFirstThumbnail() + await overlays.clicksFirstThumbnail(); // Then our player loads for the correct video - await overlays.duckPlayerLoadsFor('1') - }) + await overlays.duckPlayerLoadsFor('1'); + }); test('forces next video to play in Duck Player after UI settings change', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) + const overlays = DuckplayerOverlays.create(page, workerInfo); // Given overlays feature is enabled - await overlays.withRemoteConfig() + await overlays.withRemoteConfig(); // And my setting is 'always ask' // And the UI setting for 'play in Duck Player' is not set - await overlays.initialSetupIs('always ask') - await overlays.gotoThumbsPage() + await overlays.initialSetupIs('always ask'); + await overlays.gotoThumbsPage(); // Then overlays act as normal initially - await overlays.hoverAThumbnail() - await overlays.isVisible() + await overlays.hoverAThumbnail(); + await overlays.isVisible(); // When a UI settings update arrives - await overlays.uiChangedSettingTo('play in duck player') + await overlays.uiChangedSettingTo('play in duck player'); // And I click on the first thumbnail - await overlays.clicksFirstThumbnail() + await overlays.clicksFirstThumbnail(); // Then our player loads for the correct video - await overlays.duckPlayerLoadsFor('1') - }) - }) -}) + await overlays.duckPlayerLoadsFor('1'); + }); + }); +}); test.describe('serp proxy', () => { test('serp proxy is enabled', async ({ page }, workerInfo) => { - const overlays = DuckplayerOverlays.create(page, workerInfo) - await overlays.serpProxyEnabled() - await overlays.gotoSerpProxyPage() - await overlays.userValuesCallIsProxied() - }) -}) + const overlays = DuckplayerOverlays.create(page, workerInfo); + await overlays.serpProxyEnabled(); + await overlays.gotoSerpProxyPage(); + await overlays.userValuesCallIsProxied(); + }); +}); diff --git a/injected/integration-test/extension/inject.js b/injected/integration-test/extension/inject.js index 0d260d108..7e2a0c0ae 100644 --- a/injected/integration-test/extension/inject.js +++ b/injected/integration-test/extension/inject.js @@ -1,11 +1,11 @@ -function injectContentScript (src) { - const elem = document.head || document.documentElement - const script = document.createElement('script') - script.src = src +function injectContentScript(src) { + const elem = document.head || document.documentElement; + const script = document.createElement('script'); + script.src = src; script.onload = function () { - this.remove() - } - elem.appendChild(script) + this.remove(); + }; + elem.appendChild(script); } -injectContentScript(chrome.runtime.getURL('/contentScope.js')) +injectContentScript(chrome.runtime.getURL('/contentScope.js')); diff --git a/injected/integration-test/fingerprint.spec.js b/injected/integration-test/fingerprint.spec.js index f35a80f6d..5aa9f65c9 100644 --- a/injected/integration-test/fingerprint.spec.js +++ b/injected/integration-test/fingerprint.spec.js @@ -1,14 +1,14 @@ /** * Tests for fingerprint defenses. Ensure that fingerprinting is actually being blocked. */ -import { test as base, expect } from '@playwright/test' -import { testContextForExtension } from './helpers/harness.js' -import { createRequire } from 'node:module' +import { test as base, expect } from '@playwright/test'; +import { testContextForExtension } from './helpers/harness.js'; +import { createRequire } from 'node:module'; // eslint-disable-next-line no-redeclare -const require = createRequire(import.meta.url) +const require = createRequire(import.meta.url); -const test = testContextForExtension(base) +const test = testContextForExtension(base); const expectedFingerprintValues = { availTop: 0, @@ -18,21 +18,18 @@ const expectedFingerprintValues = { colorDepth: 24, pixelDepth: 24, productSub: '20030107', - vendorSub: '' -} + vendorSub: '', +}; -const pagePath = '/index.html' -const tests = [ - { url: `http://localhost:3220${pagePath}` }, - { url: `http://127.0.0.1:8383${pagePath}` } -] +const pagePath = '/index.html'; +const tests = [{ url: `http://localhost:3220${pagePath}` }, { url: `http://127.0.0.1:8383${pagePath}` }]; test.describe.serial('All Fingerprint Defense Tests (must run in serial)', () => { test.describe.serial('Fingerprint Defense Tests', () => { for (const _test of tests) { test(`${_test.url} should include anti-fingerprinting code`, async ({ page, altServerPort }) => { - console.log('running:', altServerPort) - await page.goto(_test.url) + console.log('running:', altServerPort); + await page.goto(_test.url); const values = await page.evaluate(() => { return { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f @@ -46,95 +43,95 @@ test.describe.serial('All Fingerprint Defense Tests (must run in serial)', () => colorDepth: screen.colorDepth, pixelDepth: screen.pixelDepth, productSub: navigator.productSub, - vendorSub: navigator.vendorSub - } - }) + vendorSub: navigator.vendorSub, + }; + }); for (const [name, prop] of Object.entries(values)) { await test.step(name, () => { - expect(prop).toEqual(expectedFingerprintValues[name]) - }) + expect(prop).toEqual(expectedFingerprintValues[name]); + }); } - await page.close() - }) + await page.close(); + }); } - }) + }); test.describe.serial('First Party Fingerprint Randomization', () => { /** * @param {import("@playwright/test").Page} page * @param {tests[number]} test */ - async function runTest (page, test) { - await page.goto(test.url) - const lib = require.resolve('@fingerprintjs/fingerprintjs/dist/fp.js') - await page.addScriptTag({ path: lib }) + async function runTest(page, test) { + await page.goto(test.url); + const lib = require.resolve('@fingerprintjs/fingerprintjs/dist/fp.js'); + await page.addScriptTag({ path: lib }); const fingerprint = await page.evaluate(() => { /* global FingerprintJS */ return (async () => { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const fp = await FingerprintJS.load() - return fp.get() - })() - }) + const fp = await FingerprintJS.load(); + return fp.get(); + })(); + }); return { canvas: fingerprint.components.canvas.value, - plugin: fingerprint.components.plugins.value - } + plugin: fingerprint.components.plugins.value, + }; } for (const testCase of tests) { test(`Fingerprints should not change amongst page loads test ${testCase.url}`, async ({ page, altServerPort }) => { - console.log('running:', altServerPort) - const result = await runTest(page, testCase) + console.log('running:', altServerPort); + const result = await runTest(page, testCase); - const result2 = await runTest(page, testCase) - expect(result.canvas).toEqual(result2.canvas) - expect(result.plugin).toEqual(result2.plugin) - }) + const result2 = await runTest(page, testCase); + expect(result.canvas).toEqual(result2.canvas); + expect(result.plugin).toEqual(result2.plugin); + }); } test('Fingerprints should not match across first parties', async ({ page, altServerPort }) => { - console.log('running:', altServerPort) - const canvas = new Set() - const plugin = new Set() + console.log('running:', altServerPort); + const canvas = new Set(); + const plugin = new Set(); for (const testCase of tests) { - const result = await runTest(page, testCase) + const result = await runTest(page, testCase); // Add the fingerprints to a set, if the result doesn't match it won't be added - canvas.add(JSON.stringify(result.canvas)) - plugin.add(JSON.stringify(result.plugin)) + canvas.add(JSON.stringify(result.canvas)); + plugin.add(JSON.stringify(result.plugin)); } // Ensure that the number of test pages match the number in the set - expect(canvas.size).toEqual(tests.length) - expect(plugin.size).toEqual(1) - }) - }) + expect(canvas.size).toEqual(tests.length); + expect(plugin.size).toEqual(1); + }); + }); test.describe.serial('Verify injected script is not visible to the page', () => { - tests.forEach(testCase => { + tests.forEach((testCase) => { test(`Fingerprints should not match across first parties ${testCase.url}`, async ({ page, altServerPort }) => { - console.log('running:', altServerPort) - await page.goto(testCase.url) + console.log('running:', altServerPort); + await page.goto(testCase.url); // give it another second just to be sure - await page.waitForTimeout(1000) + await page.waitForTimeout(1000); const sjclVal = await page.evaluate(() => { if ('sjcl' in window) { - return 'visible' + return 'visible'; } else { - return 'invisible' + return 'invisible'; } - }) + }); - expect(sjclVal).toEqual('invisible') - }) - }) - }) -}) + expect(sjclVal).toEqual('invisible'); + }); + }); + }); +}); diff --git a/injected/integration-test/harmful-apis.spec.js b/injected/integration-test/harmful-apis.spec.js index 33023631e..929f5e30f 100644 --- a/injected/integration-test/harmful-apis.spec.js +++ b/injected/integration-test/harmful-apis.spec.js @@ -1,12 +1,12 @@ -import { test, expect } from '@playwright/test' -import { readFileSync } from 'fs' -import { mockWindowsMessaging, wrapWindowsScripts } from '@duckduckgo/messaging/lib/test-utils.mjs' -import { perPlatform } from './type-helpers.mjs' -import { windowsGlobalPolyfills } from './shared.mjs' +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'fs'; +import { mockWindowsMessaging, wrapWindowsScripts } from '@duckduckgo/messaging/lib/test-utils.mjs'; +import { perPlatform } from './type-helpers.mjs'; +import { windowsGlobalPolyfills } from './shared.mjs'; test.skip('Harmful APIs protections', async ({ page }, testInfo) => { - const protection = HarmfulApisSpec.create(page, testInfo) - await protection.enabled() + const protection = HarmfulApisSpec.create(page, testInfo); + await protection.enabled(); const results = await protection.runTests(); // note that if protections are disabled, the browser will show a device selection pop-up, which will never be dismissed @@ -25,58 +25,58 @@ test.skip('Harmful APIs protections', async ({ page }, testInfo) => { 'WebMidi', 'IdleDetection', 'WebNfc', - 'StorageManager' + 'StorageManager', ].forEach((name) => { for (const result of results[name]) { - expect(result.result).toEqual(result.expected) + expect(result.result).toEqual(result.expected); } - }) -}) + }); +}); export class HarmfulApisSpec { - htmlPage = '/harmful-apis/index.html' - config = './integration-test/test-pages/harmful-apis/config/apis.json' + htmlPage = '/harmful-apis/index.html'; + config = './integration-test/test-pages/harmful-apis/config/apis.json'; /** * @param {import("@playwright/test").Page} page * @param {import("./type-helpers.mjs").Build} build * @param {import("./type-helpers.mjs").PlatformInfo} platform */ - constructor (page, build, platform) { - this.page = page - this.build = build - this.platform = platform + constructor(page, build, platform) { + this.page = page; + this.build = build; + this.platform = platform; } - async enabled () { - await this.installPolyfills() - const config = JSON.parse(readFileSync(this.config, 'utf8')) - await this.setup({ config }) - await this.page.goto(this.htmlPage) + async enabled() { + await this.installPolyfills(); + const config = JSON.parse(readFileSync(this.config, 'utf8')); + await this.setup({ config }); + await this.page.goto(this.htmlPage); } - async runTests () { + async runTests() { for (const button of await this.page.getByTestId('user-gesture-button').all()) { - await button.click() + await button.click(); } const resultsPromise = this.page.evaluate(() => { - return new Promise(resolve => { + return new Promise((resolve) => { window.addEventListener('results-ready', () => { // @ts-expect-error - this is added by the test framework - resolve(window.results) - }) - }) - }) - await this.page.getByTestId('render-results').click() - return await resultsPromise + resolve(window.results); + }); + }); + }); + await this.page.getByTestId('render-results').click(); + return await resultsPromise; } /** * In CI, the global objects such as USB might not be installed on the * version of chromium running there. */ - async installPolyfills () { - await this.page.addInitScript(windowsGlobalPolyfills) + async installPolyfills() { + await this.page.addInitScript(windowsGlobalPolyfills); } /** @@ -84,8 +84,8 @@ export class HarmfulApisSpec { * @param {Record} params.config * @return {Promise} */ - async setup (params) { - const { config } = params + async setup(params) { + const { config } = params; // read the built file from disk and do replacements const injectedJS = wrapWindowsScripts(this.build.artifact, { @@ -93,21 +93,21 @@ export class HarmfulApisSpec { $USER_UNPROTECTED_DOMAINS$: [], $USER_PREFERENCES$: { platform: { name: 'windows' }, - debug: true - } - }) + debug: true, + }, + }); await this.page.addInitScript(mockWindowsMessaging, { messagingContext: { env: 'development', context: 'contentScopeScripts', - featureName: 'n/a' + featureName: 'n/a', }, - responses: {} - }) + responses: {}, + }); // attach the JS - await this.page.addInitScript(injectedJS) + await this.page.addInitScript(injectedJS); } /** @@ -115,9 +115,9 @@ export class HarmfulApisSpec { * @param {import("@playwright/test").Page} page * @param {import("@playwright/test").TestInfo} testInfo */ - static create (page, testInfo) { + static create(page, testInfo) { // Read the configuration object to determine which platform we're testing against - const { platformInfo, build } = perPlatform(testInfo.project.use) - return new HarmfulApisSpec(page, build, platformInfo) + const { platformInfo, build } = perPlatform(testInfo.project.use); + return new HarmfulApisSpec(page, build, platformInfo); } } diff --git a/injected/integration-test/helpers/harness.js b/injected/integration-test/helpers/harness.js index 58cc1e8b6..7007225b3 100644 --- a/injected/integration-test/helpers/harness.js +++ b/injected/integration-test/helpers/harness.js @@ -1,87 +1,81 @@ /* global process */ -import { mkdtempSync, rmSync } from 'node:fs' -import { tmpdir } from 'os' -import { join } from 'path' -import { chromium, firefox } from '@playwright/test' -import { fork } from 'node:child_process' +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { chromium, firefox } from '@playwright/test'; +import { fork } from 'node:child_process'; -const DATA_DIR_PREFIX = 'ddg-temp-' +const DATA_DIR_PREFIX = 'ddg-temp-'; /** * A single place * @param {typeof import("@playwright/test").test} test */ -export function testContextForExtension (test) { +export function testContextForExtension(test) { return test.extend({ context: async ({ browserName }, use) => { - const tmpDirPrefix = join(tmpdir(), DATA_DIR_PREFIX) - const dataDir = mkdtempSync(tmpDirPrefix) - const browserTypes = { chromium, firefox } + const tmpDirPrefix = join(tmpdir(), DATA_DIR_PREFIX); + const dataDir = mkdtempSync(tmpDirPrefix); + const browserTypes = { chromium, firefox }; const launchOptions = { devtools: true, headless: false, viewport: { width: 1920, - height: 1080 + height: 1080, }, - args: [ - '--disable-extensions-except=integration-test/extension', - '--load-extension=integration-test/extension' - ] - } + args: ['--disable-extensions-except=integration-test/extension', '--load-extension=integration-test/extension'], + }; - const context = await browserTypes[browserName].launchPersistentContext( - dataDir, - launchOptions - ) + const context = await browserTypes[browserName].launchPersistentContext(dataDir, launchOptions); // actually run the tests - await use(context) + await use(context); // clean up - await context.close() + await context.close(); // Clean up temporary data directory - rmSync(dataDir, { recursive: true, force: true }) + rmSync(dataDir, { recursive: true, force: true }); }, altServerPort: async ({ browserName }, use) => { - console.log('browserName:', browserName) + console.log('browserName:', browserName); const serverScript = fork('./scripts/server.mjs', { env: { ...process.env, SERVER_DIR: 'integration-test/test-pages', - SERVER_PORT: '8383' - } - }) + SERVER_PORT: '8383', + }, + }); const opened = new Promise((resolve, reject) => { - serverScript.on('message', (/** @type {any} */resp) => { + serverScript.on('message', (/** @type {any} */ resp) => { if (typeof resp.port === 'number') { - resolve(resp.port) + resolve(resp.port); } else { - reject(resp.port) + reject(resp.port); } - }) - }) + }); + }); const closed = new Promise((resolve, reject) => { serverScript.on('close', (err) => { if (err) { - reject(new Error('server did not exit, code: ' + err)) + reject(new Error('server did not exit, code: ' + err)); } else { - resolve(null) + resolve(null); } - }) + }); serverScript.on('error', () => { - reject(new Error('server errored')) - }) - }) - - const port = await opened - await use(port) - serverScript.kill() - await closed - } - }) + reject(new Error('server errored')); + }); + }); + + const port = await opened; + await use(port); + serverScript.kill(); + await closed; + }, + }); } /** @@ -96,30 +90,30 @@ export function testContextForExtension (test) { * @param {"extension" | "script"} [kind] - if 'extension', the script will be loaded separately. if 'script' we'll append a script tag * @returns {Promise} */ -export async function gotoAndWait (page, urlString, args = {}, evalBeforeInit = null, kind = 'extension') { +export async function gotoAndWait(page, urlString, args = {}, evalBeforeInit = null, kind = 'extension') { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, search] = urlString.split('?') - const searchParams = new URLSearchParams(search) + const [_, search] = urlString.split('?'); + const searchParams = new URLSearchParams(search); // Append the flag so that the script knows to wait for incoming args. - searchParams.append('wait-for-init-args', 'true') + searchParams.append('wait-for-init-args', 'true'); - await page.goto(urlString + '?' + searchParams.toString()) + await page.goto(urlString + '?' + searchParams.toString()); if (kind === 'script') { await page.addScriptTag({ - url: './build/contentScope.js' - }) + url: './build/contentScope.js', + }); } // wait until contentScopeFeatures.load() has completed await page.waitForFunction(() => { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - return window.__content_scope_status === 'loaded' - }) + return window.__content_scope_status === 'loaded'; + }); if (evalBeforeInit) { - await page.evaluate(evalBeforeInit) + await page.evaluate(evalBeforeInit); } const evalString = ` @@ -128,14 +122,14 @@ export async function gotoAndWait (page, urlString, args = {}, evalBeforeInit = const evt = new CustomEvent('content-scope-init-args', { detail }) document.dispatchEvent(evt); })(); - ` + `; - await page.evaluate(evalString) + await page.evaluate(evalString); // wait until contentScopeFeatures.init(args) has completed await page.waitForFunction(() => { - window.dispatchEvent(new Event('content-scope-init-complete')) + window.dispatchEvent(new Event('content-scope-init-complete')); // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - return window.__content_scope_status === 'initialized' - }) + return window.__content_scope_status === 'initialized'; + }); } diff --git a/injected/integration-test/navigator-interface.spec.js b/injected/integration-test/navigator-interface.spec.js index 5e039e28d..9873acf8b 100644 --- a/injected/integration-test/navigator-interface.spec.js +++ b/injected/integration-test/navigator-interface.spec.js @@ -1,24 +1,22 @@ /** * Tests for injecting navigator.duckduckgo into the page */ -import { test as base, expect } from '@playwright/test' -import { gotoAndWait, testContextForExtension } from './helpers/harness.js' +import { test as base, expect } from '@playwright/test'; +import { gotoAndWait, testContextForExtension } from './helpers/harness.js'; -const test = testContextForExtension(base) +const test = testContextForExtension(base); test.describe('Ensure navigator interface is injected', () => { test('should expose navigator.navigator.isDuckDuckGo(): Promise and platform === "extension"', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { platform: { name: 'extension' } }) - const isDuckDuckGoResult = await page.evaluate( - () => { - // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const fn = navigator.duckduckgo?.isDuckDuckGo - return fn() - } - ) - expect(isDuckDuckGoResult).toEqual(true) + await gotoAndWait(page, '/blank.html', { platform: { name: 'extension' } }); + const isDuckDuckGoResult = await page.evaluate(() => { + // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f + const fn = navigator.duckduckgo?.isDuckDuckGo; + return fn(); + }); + expect(isDuckDuckGoResult).toEqual(true); - const platformResult = await page.evaluate('navigator.duckduckgo.platform === \'extension\'') - expect(platformResult).toEqual(true) - }) -}) + const platformResult = await page.evaluate("navigator.duckduckgo.platform === 'extension'"); + expect(platformResult).toEqual(true); + }); +}); diff --git a/injected/integration-test/page-objects/broker-protection.js b/injected/integration-test/page-objects/broker-protection.js index acfcd525f..98e5a30c7 100644 --- a/injected/integration-test/page-objects/broker-protection.js +++ b/injected/integration-test/page-objects/broker-protection.js @@ -1,5 +1,5 @@ -import { expect } from '@playwright/test' -import { readFileSync } from 'fs' +import { expect } from '@playwright/test'; +import { readFileSync } from 'fs'; import { mockWebkitMessaging, readOutgoingMessages, @@ -7,9 +7,9 @@ import { wrapWebkitScripts, simulateSubscriptionMessage, wrapWindowsScripts, - mockWindowsMessaging -} from '@duckduckgo/messaging/lib/test-utils.mjs' -import { perPlatform } from '../type-helpers.mjs' + mockWindowsMessaging, +} from '@duckduckgo/messaging/lib/test-utils.mjs'; +import { perPlatform } from '../type-helpers.mjs'; export class BrokerProtectionPage { /** @@ -17,122 +17,122 @@ export class BrokerProtectionPage { * @param {import("../type-helpers.mjs").Build} build * @param {import("@duckduckgo/messaging/lib/test-utils.mjs").PlatformInfo} platform */ - constructor (page, build, platform) { - this.page = page - this.build = build - this.platform = platform + constructor(page, build, platform) { + this.page = page; + this.build = build; + this.platform = platform; page.on('console', (msg) => { - console.log(msg.type(), msg.text()) - }) + console.log(msg.type(), msg.text()); + }); } // Given the "overlays" feature is enabled - async enabled () { - await this.setup({ config: loadConfig('enabled') }) + async enabled() { + await this.setup({ config: loadConfig('enabled') }); } /** * @param {string} page - add more pages here as you need them * @return {Promise} */ - async navigatesTo (page) { - await this.page.goto('/broker-protection/pages/' + page) + async navigatesTo(page) { + await this.page.goto('/broker-protection/pages/' + page); } /** * @param {string} selector */ - async elementIsAbsent (selector) { + async elementIsAbsent(selector) { // control - ensure the element isn't there first - const e = await this.page.$(selector) - expect(e).toBeNull() + const e = await this.page.$(selector); + expect(e).toBeNull(); } /** * @return {Promise} */ - async isFormFilled () { - await expect(this.page.getByLabel('First Name:', { exact: true })).toHaveValue('John') - await expect(this.page.getByLabel('Last Name:', { exact: true })).toHaveValue('Smith') - await expect(this.page.getByLabel('Phone Number:', { exact: true })).toHaveValue(/^\d{10}$/) - await expect(this.page.getByLabel('Street Address:', { exact: true })).toHaveValue(/^\d+ [A-Za-z]+(?: [A-Za-z]+)?$/) - await expect(this.page.locator('#state')).toHaveValue('IL') - await expect(this.page.getByLabel('Zip Code:', { exact: true })).toHaveValue(/^\d{5}$/) - - const randomValue = await this.page.getByLabel('Random number between 5 and 15:').inputValue() - const randomValueInt = parseInt(randomValue) - - expect(Number.isInteger(randomValueInt)).toBe(true) - expect(randomValueInt).toBeGreaterThanOrEqual(5) - expect(randomValueInt).toBeLessThanOrEqual(15) - - await expect(this.page.getByLabel('City & State:', { exact: true })).toHaveValue('Chicago, IL') + async isFormFilled() { + await expect(this.page.getByLabel('First Name:', { exact: true })).toHaveValue('John'); + await expect(this.page.getByLabel('Last Name:', { exact: true })).toHaveValue('Smith'); + await expect(this.page.getByLabel('Phone Number:', { exact: true })).toHaveValue(/^\d{10}$/); + await expect(this.page.getByLabel('Street Address:', { exact: true })).toHaveValue(/^\d+ [A-Za-z]+(?: [A-Za-z]+)?$/); + await expect(this.page.locator('#state')).toHaveValue('IL'); + await expect(this.page.getByLabel('Zip Code:', { exact: true })).toHaveValue(/^\d{5}$/); + + const randomValue = await this.page.getByLabel('Random number between 5 and 15:').inputValue(); + const randomValueInt = parseInt(randomValue); + + expect(Number.isInteger(randomValueInt)).toBe(true); + expect(randomValueInt).toBeGreaterThanOrEqual(5); + expect(randomValueInt).toBeLessThanOrEqual(15); + + await expect(this.page.getByLabel('City & State:', { exact: true })).toHaveValue('Chicago, IL'); } /** * @return {Promise} */ - async isCaptchaTokenFilled () { - const captchaTextArea = await this.page.$('#g-recaptcha-response') - const captchaToken = await captchaTextArea?.evaluate((element) => element.innerHTML) - expect(captchaToken).toBe('test_token') + async isCaptchaTokenFilled() { + const captchaTextArea = await this.page.$('#g-recaptcha-response'); + const captchaToken = await captchaTextArea?.evaluate((element) => element.innerHTML); + expect(captchaToken).toBe('test_token'); } /** * @return {void} */ - isExtractMatch (response, person) { - expect(person).toMatchObject(response) + isExtractMatch(response, person) { + expect(person).toMatchObject(response); } /** * @return {void} */ - isUrlMatch (response) { - expect(response.url).toBe('https://www.verecor.com/profile/search?fname=Ben&lname=Smith&state=fl&city=New-York&fage=41-50') + isUrlMatch(response) { + expect(response.url).toBe('https://www.verecor.com/profile/search?fname=Ben&lname=Smith&state=fl&city=New-York&fage=41-50'); } /** * @return {void} */ - isCaptchaMatch (response) { + isCaptchaMatch(response) { expect(response).toStrictEqual({ siteKey: '6LeCl8UUAAAAAGssOpatU5nzFXH2D7UZEYelSLTn', url: 'http://localhost:3220/broker-protection/pages/captcha.html', - type: 'recaptcha2' - }) + type: 'recaptcha2', + }); } /** * @return {void} */ - isHCaptchaMatch (response) { + isHCaptchaMatch(response) { expect(response).toStrictEqual({ siteKey: '6LeCl8UUAAAAAGssOpatU5nzFXH2D7UZEYelSLTn', url: 'http://localhost:3220/broker-protection/pages/captcha2.html', - type: 'hcaptcha' - }) + type: 'hcaptcha', + }); } /** * @return {void} */ - isQueryParamRemoved (response) { - const url = new URL(response.url) - expect(url.searchParams.toString()).toBe('') + isQueryParamRemoved(response) { + const url = new URL(response.url); + expect(url.searchParams.toString()).toBe(''); } /** * @param meta */ - responseContainsMetadata (meta) { - expect(meta.extractResults).toHaveLength(10) - expect(meta.extractResults.filter(x => x.result === true)).toHaveLength(1) - expect(meta.extractResults.filter(x => x.result === false)).toHaveLength(9) - const match = meta.extractResults.find(x => x.result === true) - expect(match.matchedFields).toMatchObject(['name', 'age', 'addressCityStateList']) - expect(match.element).toBe(undefined) - expect(match.score).toBe(3) + responseContainsMetadata(meta) { + expect(meta.extractResults).toHaveLength(10); + expect(meta.extractResults.filter((x) => x.result === true)).toHaveLength(1); + expect(meta.extractResults.filter((x) => x.result === false)).toHaveLength(9); + const match = meta.extractResults.find((x) => x.result === true); + expect(match.matchedFields).toMatchObject(['name', 'age', 'addressCityStateList']); + expect(match.element).toBe(undefined); + expect(match.score).toBe(3); } /** @@ -141,17 +141,17 @@ export class BrokerProtectionPage { * @param {string} action * @return {Promise} */ - async receivesAction (action) { - const actionJson = JSON.parse(readFileSync('./integration-test/test-pages/broker-protection/actions/' + action, 'utf8')) - await this.simulateSubscriptionMessage('onActionReceived', actionJson) + async receivesAction(action) { + const actionJson = JSON.parse(readFileSync('./integration-test/test-pages/broker-protection/actions/' + action, 'utf8')); + await this.simulateSubscriptionMessage('onActionReceived', actionJson); } /** * @param {{state: {action: Record}}} action * @return {Promise} */ - async receivesInlineAction (action) { - await this.simulateSubscriptionMessage('onActionReceived', action) + async receivesInlineAction(action) { + await this.simulateSubscriptionMessage('onActionReceived', action); } /** @@ -160,50 +160,48 @@ export class BrokerProtectionPage { * @param {'init-data.json'} action - add more action types here * @return {Promise} */ - async receivesData (action) { - const actionJson = JSON.parse(readFileSync('./integration-test/test-pages/broker-protection/data/' + action, 'utf8')) - await this.simulateSubscriptionMessage('onInit', actionJson) + async receivesData(action) { + const actionJson = JSON.parse(readFileSync('./integration-test/test-pages/broker-protection/data/' + action, 'utf8')); + await this.simulateSubscriptionMessage('onInit', actionJson); } - async sendsReadyNotification () { - const calls = await this.waitForMessage('ready') + async sendsReadyNotification() { + const calls = await this.waitForMessage('ready'); expect(calls).toMatchObject([ { payload: { context: this.messagingContext.context, featureName: 'brokerProtection', method: 'ready', - params: {} - } - } - ]) + params: {}, + }, + }, + ]); } /** * @param {string} name * @param {Record} payload */ - async simulateSubscriptionMessage (name, payload) { + async simulateSubscriptionMessage(name, payload) { await this.page.evaluate(simulateSubscriptionMessage, { messagingContext: this.messagingContext, name, payload, - injectName: this.build.name - }) + injectName: this.build.name, + }); } /** * @return {import("@duckduckgo/messaging").MessagingContext} */ - get messagingContext () { - const context = this.build.name === 'apple-isolated' - ? 'contentScopeScriptsIsolated' - : 'contentScopeScripts' + get messagingContext() { + const context = this.build.name === 'apple-isolated' ? 'contentScopeScriptsIsolated' : 'contentScopeScripts'; return { context, featureName: 'brokerProtection', - env: 'development' - } + env: 'development', + }; } /** @@ -214,64 +212,68 @@ export class BrokerProtectionPage { * @param {Record} params.config * @return {Promise} */ - async setup (params) { - const { config } = params + async setup(params) { + const { config } = params; // read the built file from disk and do replacements const wrapFn = this.build.switch({ 'apple-isolated': () => wrapWebkitScripts, - windows: () => wrapWindowsScripts - }) + windows: () => wrapWindowsScripts, + }); const injectedJS = wrapFn(this.build.artifact, { $CONTENT_SCOPE$: config, $USER_UNPROTECTED_DOMAINS$: [], $USER_PREFERENCES$: { platform: { name: this.platform.name }, - debug: true - } - }) + debug: true, + }, + }); const mockMessaging = this.build.switch({ 'apple-isolated': () => mockWebkitMessaging, - windows: () => mockWindowsMessaging - }) + windows: () => mockWindowsMessaging, + }); await this.page.addInitScript(mockMessaging, { messagingContext: this.messagingContext, responses: { - ready: {} - } - }) + ready: {}, + }, + }); // attach the JS - await this.page.addInitScript(injectedJS) + await this.page.addInitScript(injectedJS); } /** * @param {string} method * @return {Promise} */ - async waitForMessage (method) { - await this.page.waitForFunction(waitForCallCount, { - method, - count: 1 - }, { timeout: 5000, polling: 100 }) - const calls = await this.page.evaluate(readOutgoingMessages) - return calls.filter(v => v.payload.method === method) + async waitForMessage(method) { + await this.page.waitForFunction( + waitForCallCount, + { + method, + count: 1, + }, + { timeout: 5000, polling: 100 }, + ); + const calls = await this.page.evaluate(readOutgoingMessages); + return calls.filter((v) => v.payload.method === method); } /** * @param {object} response */ - isErrorMessage (response) { + isErrorMessage(response) { // eslint-disable-next-line no-unsafe-optional-chaining - expect('error' in response[0].payload?.params?.result).toBe(true) + expect('error' in response[0].payload?.params?.result).toBe(true); } - isSuccessMessage (response) { + isSuccessMessage(response) { // eslint-disable-next-line no-unsafe-optional-chaining - expect('success' in response[0].payload?.params?.result).toBe(true) + expect('success' in response[0].payload?.params?.result).toBe(true); } /** @@ -279,10 +281,10 @@ export class BrokerProtectionPage { * @param {import("@playwright/test").Page} page * @param {import("@playwright/test").TestInfo} testInfo */ - static create (page, testInfo) { + static create(page, testInfo) { // Read the configuration object to determine which platform we're testing against - const { platformInfo, build } = perPlatform(testInfo.project.use) - return new BrokerProtectionPage(page, build, platformInfo) + const { platformInfo, build } = perPlatform(testInfo.project.use); + return new BrokerProtectionPage(page, build, platformInfo); } } @@ -290,6 +292,6 @@ export class BrokerProtectionPage { * @param {"enabled"} name * @return {Record} */ -function loadConfig (name) { - return JSON.parse(readFileSync(`./integration-test/test-pages/broker-protection/config/${name}.json`, 'utf8')) +function loadConfig(name) { + return JSON.parse(readFileSync(`./integration-test/test-pages/broker-protection/config/${name}.json`, 'utf8')); } diff --git a/injected/integration-test/page-objects/duckplayer-overlays.js b/injected/integration-test/page-objects/duckplayer-overlays.js index 7d70afa6b..af5faeac8 100644 --- a/injected/integration-test/page-objects/duckplayer-overlays.js +++ b/injected/integration-test/page-objects/duckplayer-overlays.js @@ -1,76 +1,80 @@ -import { readFileSync } from 'fs' +import { readFileSync } from 'fs'; import { mockAndroidMessaging, - mockResponses, mockWebkitMessaging, + mockResponses, + mockWebkitMessaging, mockWindowsMessaging, - readOutgoingMessages, simulateSubscriptionMessage, waitForCallCount, wrapWebkitScripts, - wrapWindowsScripts -} from '@duckduckgo/messaging/lib/test-utils.mjs' -import { expect } from '@playwright/test' -import { perPlatform } from '../type-helpers.mjs' -import { windowsGlobalPolyfills } from '../shared.mjs' + readOutgoingMessages, + simulateSubscriptionMessage, + waitForCallCount, + wrapWebkitScripts, + wrapWindowsScripts, +} from '@duckduckgo/messaging/lib/test-utils.mjs'; +import { expect } from '@playwright/test'; +import { perPlatform } from '../type-helpers.mjs'; +import { windowsGlobalPolyfills } from '../shared.mjs'; // Every possible combination of UserValues const userValues = { /** @type {import("../../src/features/duck-player.js").UserValues} */ 'always ask': { privatePlayerMode: { alwaysAsk: {} }, - overlayInteracted: false + overlayInteracted: false, }, /** @type {import("../../src/features/duck-player.js").UserValues} */ 'always ask remembered': { privatePlayerMode: { alwaysAsk: {} }, - overlayInteracted: true + overlayInteracted: true, }, /** @type {import("../../src/features/duck-player.js").UserValues} */ enabled: { privatePlayerMode: { enabled: {} }, - overlayInteracted: false + overlayInteracted: false, }, /** @type {import("../../src/features/duck-player.js").UserValues} */ disabled: { privatePlayerMode: { disabled: {} }, - overlayInteracted: false - } -} + overlayInteracted: false, + }, +}; // Possible UI Settings const uiSettings = { 'play in duck player': { - playInDuckPlayer: true - } -} + playInDuckPlayer: true, + }, +}; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const configFiles = /** @type {const} */([ +const configFiles = /** @type {const} */ ([ 'overlays.json', 'overlays-live.json', 'disabled.json', 'thumbnail-overlays-disabled.json', 'click-interceptions-disabled.json', 'video-overlays-disabled.json', - 'video-alt-selectors.json' -]) + 'video-alt-selectors.json', +]); export class DuckplayerOverlays { - overlaysPage = '/duckplayer/pages/overlays.html' - playerPage = '/duckplayer/pages/player.html' - videoAltSelectors = '/duckplayer/pages/video-alt-selectors.html' - serpProxyPage = '/duckplayer/pages/serp-proxy.html' - mobile = new DuckplayerOverlaysMobile(this) - pixels = new DuckplayerOverlayPixels(this) + overlaysPage = '/duckplayer/pages/overlays.html'; + playerPage = '/duckplayer/pages/player.html'; + videoAltSelectors = '/duckplayer/pages/video-alt-selectors.html'; + serpProxyPage = '/duckplayer/pages/serp-proxy.html'; + mobile = new DuckplayerOverlaysMobile(this); + pixels = new DuckplayerOverlayPixels(this); /** * @param {import("@playwright/test").Page} page * @param {import("../type-helpers.mjs").Build} build * @param {import("@duckduckgo/messaging/lib/test-utils.mjs").PlatformInfo} platform */ - constructor (page, build, platform) { - this.page = page - this.build = build - this.platform = platform + constructor(page, build, platform) { + this.page = page; + this.build = build; + this.platform = platform; page.on('console', (msg) => { - console.log(msg.type(), msg.text()) - }) + console.log(msg.type(), msg.text()); + }); } /** @@ -78,73 +82,76 @@ export class DuckplayerOverlays { * @param {'default' | 'cookie_banner'} [params.variant] * @return {Promise} */ - async gotoThumbsPage (params = {}) { - const { variant = 'default' } = params + async gotoThumbsPage(params = {}) { + const { variant = 'default' } = params; const urlParams = new URLSearchParams({ - variant - }) - await this.page.goto(this.overlaysPage + '?' + urlParams.toString()) + variant, + }); + await this.page.goto(this.overlaysPage + '?' + urlParams.toString()); } - async dismissCookies () { + async dismissCookies() { // cookie banner - await this.page.getByRole('button', { name: 'Reject the use of cookies and other data for the purposes described' }).click() + await this.page.getByRole('button', { name: 'Reject the use of cookies and other data for the purposes described' }).click(); } - async gotoYoutubeHomepage () { - await this.page.goto('https://www.youtube.com') + async gotoYoutubeHomepage() { + await this.page.goto('https://www.youtube.com'); // await this.dismissCookies() } - async gotoYoutubeVideo () { - await this.page.goto('https://www.youtube.com/watch?v=nfWlot6h_JM') + async gotoYoutubeVideo() { + await this.page.goto('https://www.youtube.com/watch?v=nfWlot6h_JM'); // await this.dismissCookies() } - async gotoYoutubeSearchPageForMovie () { - await this.page.goto('https://www.youtube.com/results?search_query=snatch') + async gotoYoutubeSearchPageForMovie() { + await this.page.goto('https://www.youtube.com/results?search_query=snatch'); // await this.dismissCookies() } /** * @return {Promise} */ - async clicksFirstThumbnail () { - const elem = this.page.locator('a[href^="/watch?v"]:has(img)').first() - const link = await elem.getAttribute('href') - if (!link) throw new Error('link must exist') - await elem.click({ force: true }) - const url = new URL(link, 'https://youtube.com') - const v = url.searchParams.get('v') - if (!v) throw new Error('v param must exist') - return v + async clicksFirstThumbnail() { + const elem = this.page.locator('a[href^="/watch?v"]:has(img)').first(); + const link = await elem.getAttribute('href'); + if (!link) throw new Error('link must exist'); + await elem.click({ force: true }); + const url = new URL(link, 'https://youtube.com'); + const v = url.searchParams.get('v'); + if (!v) throw new Error('v param must exist'); + return v; } - async clicksFirstShortsThumbnail () { - await this.page.locator('[href*="/shorts"] img').first().click({ force: true }) + async clicksFirstShortsThumbnail() { + await this.page.locator('[href*="/shorts"] img').first().click({ force: true }); } - async showsShortsPage () { - await this.page.waitForURL(/^https:\/\/www\.youtube\.com\/shorts/, { timeout: 5000 }) + async showsShortsPage() { + await this.page.waitForURL(/^https:\/\/www\.youtube\.com\/shorts/, { timeout: 5000 }); } /** */ - async showsVideoPageFor (videoID) { - await this.page.waitForURL((url) => { - if (url.pathname === '/watch') { - if (url.searchParams.get('v') === videoID) return true - } - return false - }, { timeout: 1000 }) + async showsVideoPageFor(videoID) { + await this.page.waitForURL( + (url) => { + if (url.pathname === '/watch') { + if (url.searchParams.get('v') === videoID) return true; + } + return false; + }, + { timeout: 1000 }, + ); } /** * @param {string} requestUrl */ - opensShort (requestUrl) { - const url = new URL(requestUrl) - expect(url.pathname).toBe('/shorts/1') + opensShort(requestUrl) { + const url = new URL(requestUrl); + expect(url.pathname).toBe('/shorts/1'); } /** @@ -154,74 +161,74 @@ export class DuckplayerOverlays { * @param {'playerPage' | 'videoAltSelectors'} [params.pageType] * - we are replicating different strategies in the HTML to capture regressions/bugs */ - async gotoPlayerPage (params = {}) { - const { variant = 'default', videoID = '123', pageType = 'playerPage' } = params + async gotoPlayerPage(params = {}) { + const { variant = 'default', videoID = '123', pageType = 'playerPage' } = params; const urlParams = new URLSearchParams([ ['v', videoID], - ['variant', variant] - ]) + ['variant', variant], + ]); - const page = this[pageType] + const page = this[pageType]; - await this.page.goto(page + '?' + urlParams.toString()) + await this.page.goto(page + '?' + urlParams.toString()); } - async gotoSerpProxyPage () { - await this.page.goto(this.serpProxyPage) + async gotoSerpProxyPage() { + await this.page.goto(this.serpProxyPage); } - async userValuesCallIsProxied () { - const calls = await this.page.evaluate(readOutgoingMessages) - const message = calls[0] - const { id, ...rest } = message.payload + async userValuesCallIsProxied() { + const calls = await this.page.evaluate(readOutgoingMessages); + const message = calls[0]; + const { id, ...rest } = message.payload; // just a sanity-check to ensure a none-empty string was used as the id - expect(typeof id).toBe('string') - expect(id.length).toBeGreaterThan(10) + expect(typeof id).toBe('string'); + expect(id.length).toBeGreaterThan(10); // assert on the payload, minus the ID expect(rest).toMatchObject({ context: this.messagingContext, featureName: 'duckPlayer', params: {}, - method: 'getUserValues' - }) + method: 'getUserValues', + }); } - async overlayBlocksVideo () { - await this.page.locator('ddg-video-overlay').waitFor({ state: 'visible', timeout: 1000 }) - await this.page.getByRole('link', { name: 'Turn On Duck Player' }).waitFor({ state: 'visible', timeout: 1000 }) + async overlayBlocksVideo() { + await this.page.locator('ddg-video-overlay').waitFor({ state: 'visible', timeout: 1000 }); + await this.page.getByRole('link', { name: 'Turn On Duck Player' }).waitFor({ state: 'visible', timeout: 1000 }); await this.page .getByText('What you watch in DuckDuckGo won’t influence your recommendations on YouTube.') - .waitFor({ timeout: 100 }) + .waitFor({ timeout: 100 }); } /** * @param {object} [params] * @param {string} [params.videoID] */ - async hasWatchLinkFor (params = {}) { - const { videoID = '123' } = params + async hasWatchLinkFor(params = {}) { + const { videoID = '123' } = params; // this is added because 'getAttribute' does not auto-wait await expect(async () => { - const link = await this.page.getByRole('link', { name: 'Turn On Duck Player' }).getAttribute('href') - expect(link).toEqual('duck://player/' + videoID) - }).toPass({ timeout: 5000 }) + const link = await this.page.getByRole('link', { name: 'Turn On Duck Player' }).getAttribute('href'); + expect(link).toEqual('duck://player/' + videoID); + }).toPass({ timeout: 5000 }); } /** * @param {object} [params] * @param {string} [params.videoID] */ - async clickRelatedThumb (params = {}) { - const { videoID = '123' } = params - await this.page.locator(`a[href="/watch?v=${videoID}"]`).click({ force: true }) - await this.page.waitForURL((url) => url.searchParams.get('v') === videoID) + async clickRelatedThumb(params = {}) { + const { videoID = '123' } = params; + await this.page.locator(`a[href="/watch?v=${videoID}"]`).click({ force: true }); + await this.page.waitForURL((url) => url.searchParams.get('v') === videoID); } - async smallOverlayShows () { - await this.page.getByRole('link', { name: 'Duck Player', exact: true }).waitFor({ state: 'attached' }) + async smallOverlayShows() { + await this.page.getByRole('link', { name: 'Duck Player', exact: true }).waitFor({ state: 'attached' }); } /** @@ -229,39 +236,36 @@ export class DuckplayerOverlays { * @param {configFiles[number]} [params.json="overlays"] - default is settings for localhost * @param {string} [params.locale] - optional locale */ - async withRemoteConfig (params = {}) { - const { - json = 'overlays.json', - locale = 'en' - } = params + async withRemoteConfig(params = {}) { + const { json = 'overlays.json', locale = 'en' } = params; - await this.setup({ config: loadConfig(json), locale }) + await this.setup({ config: loadConfig(json), locale }); } - async serpProxyEnabled () { - const config = loadConfig('overlays.json') - const domains = config.features.duckPlayer.settings.domains[0].patchSettings - config.features.duckPlayer.settings.domains[0].patchSettings = domains.filter(x => x.path === '/overlays/serpProxy/state') - await this.setup({ config, locale: 'en' }) + async serpProxyEnabled() { + const config = loadConfig('overlays.json'); + const domains = config.features.duckPlayer.settings.domains[0].patchSettings; + config.features.duckPlayer.settings.domains[0].patchSettings = domains.filter((x) => x.path === '/overlays/serpProxy/state'); + await this.setup({ config, locale: 'en' }); } - async videoOverlayDoesntShow () { - expect(await this.page.locator('ddg-video-overlay').count()).toBe(0) + async videoOverlayDoesntShow() { + expect(await this.page.locator('ddg-video-overlay').count()).toBe(0); } /** * @param {keyof userValues} setting * @return {Promise} */ - async userSettingIs (setting) { + async userSettingIs(setting) { await this.page.addInitScript(mockResponses, { responses: { initialSetup: { userValues: userValues[setting], - ui: {} - } - } - }) + ui: {}, + }, + }, + }); } /** @@ -269,159 +273,159 @@ export class DuckplayerOverlays { * @param {keyof uiSettings} [uiSetting] * @return {Promise} */ - async initialSetupIs (userValueSetting, uiSetting) { + async initialSetupIs(userValueSetting, uiSetting) { const initialSetupResponse = { userValues: userValues[userValueSetting], - ui: {} - } + ui: {}, + }; if (uiSetting && uiSettings[uiSetting]) { - initialSetupResponse.ui = uiSettings[uiSetting] + initialSetupResponse.ui = uiSettings[uiSetting]; } await this.page.addInitScript(mockResponses, { responses: { - initialSetup: initialSetupResponse - } - }) + initialSetup: initialSetupResponse, + }, + }); } /** * @param {keyof userValues} setting */ - async userChangedSettingTo (setting) { + async userChangedSettingTo(setting) { await this.page.evaluate(simulateSubscriptionMessage, { messagingContext: { context: this.messagingContext, featureName: 'duckPlayer', - env: 'development' + env: 'development', }, name: 'onUserValuesChanged', payload: userValues[setting], - injectName: this.build.name - }) + injectName: this.build.name, + }); } /** * @param {keyof uiSettings} setting */ - async uiChangedSettingTo (setting) { + async uiChangedSettingTo(setting) { await this.page.evaluate(simulateSubscriptionMessage, { messagingContext: { context: this.messagingContext, featureName: 'duckPlayer', - env: 'development' + env: 'development', }, name: 'onUIValuesChanged', payload: uiSettings[setting], - injectName: this.build.name - }) + injectName: this.build.name, + }); } - async overlaysDisabled () { + async overlaysDisabled() { // load original config - const config = loadConfig('overlays.json') + const config = loadConfig('overlays.json'); // remove all domains from 'overlays', this disables the feature - config.features.duckPlayer.settings.domains = [] - await this.setup({ config, locale: 'en' }) + config.features.duckPlayer.settings.domains = []; + await this.setup({ config, locale: 'en' }); } - async hoverAThumbnail () { - await this.page.locator('.thumbnail[href^="/watch"]').first().hover({ force: true }) + async hoverAThumbnail() { + await this.page.locator('.thumbnail[href^="/watch"]').first().hover({ force: true }); } - async hoverNthThumbnail (index = 0) { - await this.page.locator('.thumbnail[href^="/watch"]').nth(index).hover({ force: true }) + async hoverNthThumbnail(index = 0) { + await this.page.locator('.thumbnail[href^="/watch"]').nth(index).hover({ force: true }); } - async clickNthThumbnail (index = 0) { - await this.page.locator('.thumbnail[href^="/watch"]').nth(index).click({ force: true }) + async clickNthThumbnail(index = 0) { + await this.page.locator('.thumbnail[href^="/watch"]').nth(index).click({ force: true }); } /** * @param {string} regionSelector */ - async hoverAThumbnailInExcludedRegion (regionSelector) { - await this.page.locator(`${regionSelector} a[href^="/watch"]`).first().hover() + async hoverAThumbnailInExcludedRegion(regionSelector) { + await this.page.locator(`${regionSelector} a[href^="/watch"]`).first().hover(); } - async hoverAYouTubeThumbnail () { - await this.page.locator('a.ytd-thumbnail[href^="/watch"]').first().hover({ force: true }) + async hoverAYouTubeThumbnail() { + await this.page.locator('a.ytd-thumbnail[href^="/watch"]').first().hover({ force: true }); } - async hoverAMovieThumb () { - await this.page.locator('ytd-movie-renderer a.ytd-thumbnail[href^="/watch"]').first().hover({ force: true }) + async hoverAMovieThumb() { + await this.page.locator('ytd-movie-renderer a.ytd-thumbnail[href^="/watch"]').first().hover({ force: true }); } - async hoverShort () { + async hoverShort() { // this should auto-wait for our test code to modify the DOM like YouTube does - await this.page.getByRole('heading', { name: 'Shorts', exact: true }).scrollIntoViewIfNeeded() - await this.page.locator('a[href*="/shorts"]').first().hover({ force: true }) + await this.page.getByRole('heading', { name: 'Shorts', exact: true }).scrollIntoViewIfNeeded(); + await this.page.locator('a[href*="/shorts"]').first().hover({ force: true }); } - async clickDDGOverlay () { - await this.hoverAThumbnail() - await this.page.locator('.ddg-play-privately').click({ force: true }) + async clickDDGOverlay() { + await this.hoverAThumbnail(); + await this.page.locator('.ddg-play-privately').click({ force: true }); } - async isVisible () { - await this.page.locator('.ddg-play-privately').waitFor({ state: 'attached', timeout: 1000 }) + async isVisible() { + await this.page.locator('.ddg-play-privately').waitFor({ state: 'attached', timeout: 1000 }); } - async secondOverlayExistsOnVideo () { - const elements = await this.page.$$('.ddg-play-privately') - expect(elements.length).toBe(2) - await this.page.locator('#player .html5-video-player .ddg-overlay[data-size="video-player"]').waitFor({ timeout: 1000 }) + async secondOverlayExistsOnVideo() { + const elements = await this.page.$$('.ddg-play-privately'); + expect(elements.length).toBe(2); + await this.page.locator('#player .html5-video-player .ddg-overlay[data-size="video-player"]').waitFor({ timeout: 1000 }); } - async overlaysDontShow () { - const elements = await this.page.locator('.ddg-overlay.ddg-overlay-hover').count() + async overlaysDontShow() { + const elements = await this.page.locator('.ddg-overlay.ddg-overlay-hover').count(); // if the element exists, assert that it is hidden if (elements > 0) { const style = await this.page.evaluate(() => { - const div = /** @type {HTMLDivElement|null} */(document.querySelector('.ddg-overlay.ddg-overlay-hover')) + const div = /** @type {HTMLDivElement|null} */ (document.querySelector('.ddg-overlay.ddg-overlay-hover')); if (div) { - return div.style.display + return div.style.display; } - return '' - }) + return ''; + }); - expect(style).not.toEqual('block') + expect(style).not.toEqual('block'); } // if we get here, the element was absent } - async turnOnDuckPlayer () { - const action = () => this.page.getByRole('link', { name: 'Turn On Duck Player' }).click() + async turnOnDuckPlayer() { + const action = () => this.page.getByRole('link', { name: 'Turn On Duck Player' }).click(); await this.build.switch({ 'apple-isolated': async () => { - await action() - await this.duckPlayerLoadsFor('123') + await action(); + await this.duckPlayerLoadsFor('123'); }, windows: async () => { - const failure = new Promise(resolve => { - this.page.context().on('requestfailed', f => { - if (f.url().startsWith('duck')) resolve(f.url()) - }) - }) + const failure = new Promise((resolve) => { + this.page.context().on('requestfailed', (f) => { + if (f.url().startsWith('duck')) resolve(f.url()); + }); + }); - await action() + await action(); // assert the page tried to navigate to duck player - expect(await failure).toEqual('duck://player/123') - } - }) + expect(await failure).toEqual('duck://player/123'); + }, + }); } - async noThanks () { - await this.page.getByText('No Thanks').click() + async noThanks() { + await this.page.getByText('No Thanks').click(); } - async rememberMyChoice () { - await this.page.getByText('Remember my choice').click() + async rememberMyChoice() { + await this.page.getByText('Remember my choice').click(); } /** @@ -431,32 +435,32 @@ export class DuckplayerOverlays { * @param {string} id * @return {Promise} */ - async duckPlayerLoadsFor (id) { - const messages = await this.waitForMessage('openDuckPlayer') + async duckPlayerLoadsFor(id) { + const messages = await this.waitForMessage('openDuckPlayer'); expect(messages).toMatchObject([ { payload: { context: this.messagingContext, featureName: 'duckPlayer', params: { - href: 'duck://player/' + id + href: 'duck://player/' + id, }, - method: 'openDuckPlayer' - } - } - ]) + method: 'openDuckPlayer', + }, + }, + ]); } - async duckPlayerLoadedTimes (times = 0) { + async duckPlayerLoadedTimes(times = 0) { /** @type {UnstableMockCall[]} */ - const calls = await this.page.evaluate(readOutgoingMessages) - const opened = calls.filter(call => { + const calls = await this.page.evaluate(readOutgoingMessages); + const opened = calls.filter((call) => { if ('method' in call.payload) { - return call.payload.method === 'openDuckPlayer' + return call.payload.method === 'openDuckPlayer'; } - return false - }) - expect(opened.length).toBe(times) + return false; + }); + expect(opened.length).toBe(times); } /** @@ -468,8 +472,8 @@ export class DuckplayerOverlays { * @param {string} params.locale * @return {Promise} */ - async setup (params) { - const { config, locale } = params + async setup(params) { + const { config, locale } = params; await this.build.switch({ windows: async () => { @@ -477,22 +481,22 @@ export class DuckplayerOverlays { * In CI, the global objects such as USB might not be installed on the * version of chromium running there. */ - await this.page.addInitScript(windowsGlobalPolyfills) + await this.page.addInitScript(windowsGlobalPolyfills); }, 'apple-isolated': async () => { // noop }, android: async () => { // noop - } - }) + }, + }); // read the built file from disk and do replacements const wrapFn = this.build.switch({ 'apple-isolated': () => wrapWebkitScripts, windows: () => wrapWindowsScripts, - android: () => wrapWebkitScripts - }) + android: () => wrapWebkitScripts, + }); const injectedJS = wrapFn(this.build.artifact, { $CONTENT_SCOPE$: config, @@ -505,74 +509,78 @@ export class DuckplayerOverlays { messageCallback: 'messageCallback', messageSecret: 'duckduckgo-android-messaging-secret', javascriptInterface: this.messagingContext, - locale - } - }) + locale, + }, + }); const mockMessaging = this.build.switch({ windows: () => mockWindowsMessaging, 'apple-isolated': () => mockWebkitMessaging, - android: () => mockAndroidMessaging - }) + android: () => mockAndroidMessaging, + }); await this.page.addInitScript(mockMessaging, { messagingContext: { env: 'development', context: this.messagingContext, - featureName: 'duckPlayer' + featureName: 'duckPlayer', }, responses: { initialSetup: { userValues: { privatePlayerMode: { alwaysAsk: {} }, - overlayInteracted: false + overlayInteracted: false, }, - ui: {} + ui: {}, }, getUserValues: { privatePlayerMode: { alwaysAsk: {} }, - overlayInteracted: false + overlayInteracted: false, }, setUserValues: { privatePlayerMode: { alwaysAsk: {} }, - overlayInteracted: false + overlayInteracted: false, }, - sendDuckPlayerPixel: {} - } - }) + sendDuckPlayerPixel: {}, + }, + }); // attach the JS - await this.page.addInitScript(injectedJS) + await this.page.addInitScript(injectedJS); } /** * @param {string} method */ - async waitForMessage (method) { - await this.page.waitForFunction(waitForCallCount, { - method, - count: 1 - }, { timeout: 3000, polling: 100 }) - const calls = await this.page.evaluate(readOutgoingMessages) - return calls.filter(v => v.payload.method === method) + async waitForMessage(method) { + await this.page.waitForFunction( + waitForCallCount, + { + method, + count: 1, + }, + { timeout: 3000, polling: 100 }, + ); + const calls = await this.page.evaluate(readOutgoingMessages); + return calls.filter((v) => v.payload.method === method); } /** * @param {keyof userValues} setting * @return {Promise} */ - async userSettingWasUpdatedTo (setting) { - const messages = await this.waitForMessage('setUserValues') + async userSettingWasUpdatedTo(setting) { + const messages = await this.waitForMessage('setUserValues'); expect(messages).toMatchObject([ { payload: { context: this.messagingContext, featureName: 'duckPlayer', params: userValues[setting], - method: 'setUserValues' - } - } - ]) + method: 'setUserValues', + }, + }, + ]); } /** @@ -580,46 +588,48 @@ export class DuckplayerOverlays { * @param {import("@playwright/test").Page} page * @param {import("@playwright/test").TestInfo} testInfo */ - static create (page, testInfo) { + static create(page, testInfo) { // Read the configuration object to determine which platform we're testing against - const { platformInfo, build } = perPlatform(testInfo.project.use) - return new DuckplayerOverlays(page, build, platformInfo) + const { platformInfo, build } = perPlatform(testInfo.project.use); + return new DuckplayerOverlays(page, build, platformInfo); } - get messagingContext () { - return this.build.name === 'apple-isolated' - ? 'contentScopeScriptsIsolated' - : 'contentScopeScripts' + get messagingContext() { + return this.build.name === 'apple-isolated' ? 'contentScopeScriptsIsolated' : 'contentScopeScripts'; } /** * @return {Promise} */ - requestWillFail () { + requestWillFail() { return new Promise((resolve, reject) => { // on windows it will be a failed request const timer = setTimeout(() => { - reject(new Error('timed out')) - }, 5000) + reject(new Error('timed out')); + }, 5000); this.page.on('framenavigated', (req) => { - clearTimeout(timer) - resolve(req.url()) - }) - }) + clearTimeout(timer); + resolve(req.url()); + }); + }); } /** * Checks for presence of default overlay copy */ - async overlayCopyIsDefault () { - await this.page.locator('ddg-video-overlay').waitFor({ state: 'visible', timeout: 1000 }) - await this.page.getByText('Turn on Duck Player to watch without targeted ads', { exact: true }).waitFor({ state: 'visible', timeout: 1000 }) - await this.page.getByText('What you watch in DuckDuckGo won’t influence your recommendations on YouTube.', { exact: true }).waitFor({ state: 'visible', timeout: 1000 }) + async overlayCopyIsDefault() { + await this.page.locator('ddg-video-overlay').waitFor({ state: 'visible', timeout: 1000 }); + await this.page + .getByText('Turn on Duck Player to watch without targeted ads', { exact: true }) + .waitFor({ state: 'visible', timeout: 1000 }); + await this.page + .getByText('What you watch in DuckDuckGo won’t influence your recommendations on YouTube.', { exact: true }) + .waitFor({ state: 'visible', timeout: 1000 }); - await this.page.getByRole('link', { name: 'Turn On Duck Player' }).waitFor({ state: 'visible', timeout: 1000 }) - await this.page.getByRole('button', { name: 'No Thanks' }).waitFor({ state: 'visible', timeout: 1000 }) + await this.page.getByRole('link', { name: 'Turn On Duck Player' }).waitFor({ state: 'visible', timeout: 1000 }); + await this.page.getByRole('button', { name: 'No Thanks' }).waitFor({ state: 'visible', timeout: 1000 }); - await this.page.getByLabel('Remember my choice').waitFor({ state: 'visible', timeout: 1000 }) + await this.page.getByLabel('Remember my choice').waitFor({ state: 'visible', timeout: 1000 }); } } @@ -627,35 +637,35 @@ class DuckplayerOverlaysMobile { /** * @param {DuckplayerOverlays} overlays */ - constructor (overlays) { - this.overlays = overlays + constructor(overlays) { + this.overlays = overlays; } - async choosesWatchHere () { - const { page } = this.overlays - await page.getByRole('button', { name: 'No Thanks' }).click() + async choosesWatchHere() { + const { page } = this.overlays; + await page.getByRole('button', { name: 'No Thanks' }).click(); } - async choosesDuckPlayer () { - const { page } = this.overlays - await page.getByRole('link', { name: 'Turn On Duck Player' }).click() + async choosesDuckPlayer() { + const { page } = this.overlays; + await page.getByRole('link', { name: 'Turn On Duck Player' }).click(); } - async selectsRemember () { - const { page } = this.overlays - await page.getByRole('switch').click() + async selectsRemember() { + const { page } = this.overlays; + await page.getByRole('switch').click(); } - async overlayIsRemoved () { - const { page } = this.overlays - expect(await page.locator('ddg-video-overlay-mobile').count()).toBe(0) + async overlayIsRemoved() { + const { page } = this.overlays; + expect(await page.locator('ddg-video-overlay-mobile').count()).toBe(0); } - async opensInfo () { - const { page } = this.overlays - await page.getByLabel('Open Information Modal').click() - const messages = await this.overlays.waitForMessage('openInfo') - expect(messages).toHaveLength(1) + async opensInfo() { + const { page } = this.overlays; + await page.getByLabel('Open Information Modal').click(); + const messages = await this.overlays.waitForMessage('openInfo'); + expect(messages).toHaveLength(1); } } @@ -663,18 +673,18 @@ class DuckplayerOverlayPixels { /** * @param {DuckplayerOverlays} overlays */ - constructor (overlays) { - this.overlays = overlays + constructor(overlays) { + this.overlays = overlays; } /** * @param {{pixelName: string, params: Record}[]} pixels * @return {Promise} */ - async sendsPixels (pixels) { - const messages = await this.overlays.waitForMessage('sendDuckPlayerPixel') - const params = messages.map(x => x.payload.params) - expect(params).toMatchObject(pixels) + async sendsPixels(pixels) { + const messages = await this.overlays.waitForMessage('sendDuckPlayerPixel'); + const params = messages.map((x) => x.payload.params); + expect(params).toMatchObject(pixels); } } @@ -682,6 +692,6 @@ class DuckplayerOverlayPixels { * @param {configFiles[number]} name * @return {Record} */ -function loadConfig (name) { - return JSON.parse(readFileSync(`./integration-test/test-pages/duckplayer/config/${name}`, 'utf8')) +function loadConfig(name) { + return JSON.parse(readFileSync(`./integration-test/test-pages/duckplayer/config/${name}`, 'utf8')); } diff --git a/injected/integration-test/pages.spec.js b/injected/integration-test/pages.spec.js index b3b3c7d37..6fa817b4c 100644 --- a/injected/integration-test/pages.spec.js +++ b/injected/integration-test/pages.spec.js @@ -1,13 +1,13 @@ /** * Tests for shared pages */ -import * as fs from 'fs' -import { test as base, expect } from '@playwright/test' -import { processConfig } from '../src/utils.js' -import polyfillProcessGlobals from '../unit-test/helpers/pollyfil-for-process-globals.js' -import { gotoAndWait, testContextForExtension } from './helpers/harness.js' +import * as fs from 'fs'; +import { test as base, expect } from '@playwright/test'; +import { processConfig } from '../src/utils.js'; +import polyfillProcessGlobals from '../unit-test/helpers/pollyfil-for-process-globals.js'; +import { gotoAndWait, testContextForExtension } from './helpers/harness.js'; -const test = testContextForExtension(base) +const test = testContextForExtension(base); test.describe('Test integration pages', () => { /** @@ -16,63 +16,61 @@ test.describe('Test integration pages', () => { * @param {string} configPath * @param {string} [evalBeforeInit] */ - async function testPage (page, pageName, configPath, evalBeforeInit) { - const res = fs.readFileSync(configPath, 'utf8') - const config = JSON.parse(res.toString()) - polyfillProcessGlobals() + async function testPage(page, pageName, configPath, evalBeforeInit) { + const res = fs.readFileSync(configPath, 'utf8'); + const config = JSON.parse(res.toString()); + polyfillProcessGlobals(); /** @type {import('../src/utils.js').UserPreferences} */ const userPreferences = { platform: { - name: 'extension' + name: 'extension', }, - sessionKey: 'test' - } - const processedConfig = processConfig(config, /* userList */ [], /* preferences */ userPreferences/*, platformSpecificFeatures = [] */) + sessionKey: 'test', + }; + const processedConfig = processConfig( + config, + /* userList */ [], + /* preferences */ userPreferences /*, platformSpecificFeatures = [] */, + ); - await gotoAndWait(page, `/${pageName}?automation=true`, processedConfig, evalBeforeInit) + await gotoAndWait(page, `/${pageName}?automation=true`, processedConfig, evalBeforeInit); // Check page results - const pageResults = await page.evaluate( - () => { - let res - const promise = new Promise(resolve => { - res = resolve - }) + const pageResults = await page.evaluate(() => { + let res; + const promise = new Promise((resolve) => { + res = resolve; + }); + // @ts-expect-error - results is not defined in the type definition + if (window.results) { // @ts-expect-error - results is not defined in the type definition - if (window.results) { - // @ts-expect-error - results is not defined in the type definition - res(window.results) - } else { - window.addEventListener('results-ready', (e) => { - // @ts-expect-error - e.detail is not defined in the type definition - res(e.detail) - }) - } - return promise + res(window.results); + } else { + window.addEventListener('results-ready', (e) => { + // @ts-expect-error - e.detail is not defined in the type definition + res(e.detail); + }); } - ) + return promise; + }); for (const key in pageResults) { for (const result of pageResults[key]) { await test.step(`${key}:\n ${result.name}`, () => { - expect(result.result).toEqual(result.expected) - }) + expect(result.result).toEqual(result.expected); + }); } } } test('Web compat shims correctness', async ({ page }) => { - await testPage( - page, - 'webcompat/pages/shims.html', - `${process.cwd()}/integration-test/test-pages/webcompat/config/shims.json` - ) - }) + await testPage(page, 'webcompat/pages/shims.html', `${process.cwd()}/integration-test/test-pages/webcompat/config/shims.json`); + }); test('Properly modifies localStorage entries', async ({ page }) => { await testPage( page, 'webcompat/pages/modify-localstorage.html', - `${process.cwd()}/integration-test/test-pages/webcompat/config/modify-localstorage.json` - ) - }) -}) + `${process.cwd()}/integration-test/test-pages/webcompat/config/modify-localstorage.json`, + ); + }); +}); diff --git a/injected/integration-test/remote-pages.spec.js b/injected/integration-test/remote-pages.spec.js index 3eefe6575..bc815c222 100644 --- a/injected/integration-test/remote-pages.spec.js +++ b/injected/integration-test/remote-pages.spec.js @@ -1,37 +1,37 @@ -import { test, expect } from '@playwright/test' -import path from 'path' -import { readFileSync } from 'fs' -import { baseFeatures } from '../src/features.js' +import { test, expect } from '@playwright/test'; +import path from 'path'; +import { readFileSync } from 'fs'; +import { baseFeatures } from '../src/features.js'; -const testRoot = path.join('integration-test') +const testRoot = path.join('integration-test'); -function getHARPath (harFile) { - return path.join(testRoot, 'data', 'har', harFile) +function getHARPath(harFile) { + return path.join(testRoot, 'data', 'har', harFile); } -const css = readFileSync('../build/integration/contentScope.js', 'utf8') -const parsedConfig = {} +const css = readFileSync('../build/integration/contentScope.js', 'utf8'); +const parsedConfig = {}; // Construct a parsed config object with all base features enabled Object.keys(baseFeatures).forEach((key) => { parsedConfig[key] = { - enabled: 'enabled' - } -}) + enabled: 'enabled', + }; +}); -function wrapScript (js, replacements) { +function wrapScript(js, replacements) { for (const [find, replace] of Object.entries(replacements)) { - js = js.replace(find, JSON.stringify(replace)) + js = js.replace(find, JSON.stringify(replace)); } - return js + return js; } const tests = [ // Generated using `node scripts/generate-har.js` - { url: 'duckduckgo.com/c-s-s-says-hello', har: getHARPath('duckduckgo.com/search.har') } -] + { url: 'duckduckgo.com/c-s-s-says-hello', har: getHARPath('duckduckgo.com/search.har') }, +]; test.describe('Remotely loaded files tests', () => { - tests.forEach(testCase => { + tests.forEach((testCase) => { test(`${testCase.url} should load resources and look correct`, async ({ page }, testInfo) => { const injectedJS = wrapScript(css, { $CONTENT_SCOPE$: parsedConfig, @@ -39,18 +39,18 @@ test.describe('Remotely loaded files tests', () => { $USER_PREFERENCES$: { // @ts-expect-error - no platform key platform: { name: testInfo.project.use.platform }, - debug: true - } - }) - await page.addInitScript({ content: injectedJS }) - await page.routeFromHAR(testCase.har) - await page.goto(`https://${testCase.url}`, { waitUntil: 'networkidle' }) + debug: true, + }, + }); + await page.addInitScript({ content: injectedJS }); + await page.routeFromHAR(testCase.har); + await page.goto(`https://${testCase.url}`, { waitUntil: 'networkidle' }); const values = await page.evaluate(() => { return { - results: document.querySelectorAll('[data-layout="organic"]').length - } - }) - expect(values.results).toBeGreaterThan(1) - }) - }) -}) + results: document.querySelectorAll('[data-layout="organic"]').length, + }; + }); + expect(values.results).toBeGreaterThan(1); + }); + }); +}); diff --git a/injected/integration-test/shared.mjs b/injected/integration-test/shared.mjs index 2c741398e..1862c642b 100644 --- a/injected/integration-test/shared.mjs +++ b/injected/integration-test/shared.mjs @@ -1,23 +1,39 @@ export function windowsGlobalPolyfills() { // @ts-expect-error - testing if (typeof Bluetooth === 'undefined') { - globalThis.Bluetooth = {} - globalThis.Bluetooth.prototype = { requestDevice: async () => { /* noop */ } } + globalThis.Bluetooth = {}; + globalThis.Bluetooth.prototype = { + requestDevice: async () => { + /* noop */ + }, + }; } // @ts-expect-error - testing if (typeof USB === 'undefined') { - globalThis.USB = {} - globalThis.USB.prototype = { requestDevice: async () => { /* noop */ } } + globalThis.USB = {}; + globalThis.USB.prototype = { + requestDevice: async () => { + /* noop */ + }, + }; } // @ts-expect-error - testing if (typeof Serial === 'undefined') { - globalThis.Serial = {} - globalThis.Serial.prototype = { requestPort: async () => { /* noop */ } } + globalThis.Serial = {}; + globalThis.Serial.prototype = { + requestPort: async () => { + /* noop */ + }, + }; } // @ts-expect-error - testing if (typeof HID === 'undefined') { - globalThis.HID = {} - globalThis.HID.prototype = { requestDevice: async () => { /* noop */ } } + globalThis.HID = {}; + globalThis.HID.prototype = { + requestDevice: async () => { + /* noop */ + }, + }; } } diff --git a/injected/integration-test/test-pages/duckplayer/scripts/test.mjs b/injected/integration-test/test-pages/duckplayer/scripts/test.mjs index 86a593e54..6b62db012 100644 --- a/injected/integration-test/test-pages/duckplayer/scripts/test.mjs +++ b/injected/integration-test/test-pages/duckplayer/scripts/test.mjs @@ -1,24 +1,22 @@ -import { DDGVideoOverlayMobile } from "../../../../src/features/duckplayer/components/ddg-video-overlay-mobile.js"; -import { overlayCopyVariants } from "../../../../src/features/duckplayer/text.js"; +import { DDGVideoOverlayMobile } from '../../../../src/features/duckplayer/components/ddg-video-overlay-mobile.js'; +import { overlayCopyVariants } from '../../../../src/features/duckplayer/text.js'; -customElements.define(DDGVideoOverlayMobile.CUSTOM_TAG_NAME, DDGVideoOverlayMobile) +customElements.define(DDGVideoOverlayMobile.CUSTOM_TAG_NAME, DDGVideoOverlayMobile); -const elem = /** @type {DDGVideoOverlayMobile} */(document.createElement(DDGVideoOverlayMobile.CUSTOM_TAG_NAME)) -elem.testMode = true -elem.text = overlayCopyVariants.default +const elem = /** @type {DDGVideoOverlayMobile} */ (document.createElement(DDGVideoOverlayMobile.CUSTOM_TAG_NAME)); +elem.testMode = true; +elem.text = overlayCopyVariants.default; -elem.addEventListener('opt-in', (/** @type {CustomEvent} */e) => { - console.log('did opt in?', e.detail) -}) +elem.addEventListener('opt-in', (/** @type {CustomEvent} */ e) => { + console.log('did opt in?', e.detail); +}); -elem.addEventListener('opt-out', (/** @type {CustomEvent} */e) => { - console.log('did opt out?', e.detail) -}) +elem.addEventListener('opt-out', (/** @type {CustomEvent} */ e) => { + console.log('did opt out?', e.detail); +}); elem.addEventListener('open-info', (e) => { - console.log('did open info') -}) + console.log('did open info'); +}); document.querySelector('.html5-video-player')?.append(elem); - - diff --git a/injected/integration-test/test-pages/shared/replay.js b/injected/integration-test/test-pages/shared/replay.js index bdb1e4dfb..9e4d012aa 100644 --- a/injected/integration-test/test-pages/shared/replay.js +++ b/injected/integration-test/test-pages/shared/replay.js @@ -1,6 +1,6 @@ -function replayScript (scriptText) { - const scriptElement = document.createElement('script') - scriptElement.innerText = scriptText - document.head.appendChild(scriptElement) +function replayScript(scriptText) { + const scriptElement = document.createElement('script'); + scriptElement.innerText = scriptText; + document.head.appendChild(scriptElement); } -window.replayScript = replayScript +window.replayScript = replayScript; diff --git a/injected/integration-test/test-pages/shared/scripty.js b/injected/integration-test/test-pages/shared/scripty.js index a7f58e2e9..b1d4512fd 100644 --- a/injected/integration-test/test-pages/shared/scripty.js +++ b/injected/integration-test/test-pages/shared/scripty.js @@ -1,2 +1,2 @@ // @ts-expect-error not a global variable -window.externalScriptRan = true +window.externalScriptRan = true; diff --git a/injected/integration-test/test-pages/shared/utils.js b/injected/integration-test/test-pages/shared/utils.js index 415ea176d..3c5be823d 100644 --- a/injected/integration-test/test-pages/shared/utils.js +++ b/injected/integration-test/test-pages/shared/utils.js @@ -5,10 +5,10 @@ * @property {any} expected The expected result. */ -function buildTableCell (value, tagName = 'td') { - const td = document.createElement(tagName) - td.textContent = value - return td +function buildTableCell(value, tagName = 'td') { + const td = document.createElement(tagName); + td.textContent = value; + return td; } /** @@ -16,77 +16,77 @@ function buildTableCell (value, tagName = 'td') { * @param {Record} results The results to build the table for. * @return {Element} The table element. */ -function buildResultTable (results) { - const table = document.createElement('table') - table.className = 'results' - const thead = document.createElement('thead') - let tr = document.createElement('tr') - tr.appendChild(buildTableCell('Test', 'th')) - tr.appendChild(buildTableCell('Result', 'th')) - tr.appendChild(buildTableCell('Expected', 'th')) - thead.appendChild(tr) - table.appendChild(thead) +function buildResultTable(results) { + const table = document.createElement('table'); + table.className = 'results'; + const thead = document.createElement('thead'); + let tr = document.createElement('tr'); + tr.appendChild(buildTableCell('Test', 'th')); + tr.appendChild(buildTableCell('Result', 'th')); + tr.appendChild(buildTableCell('Expected', 'th')); + thead.appendChild(tr); + table.appendChild(thead); for (const name in results) { - const resultSection = results[name] - const tbody = document.createElement('tbody') - tr = document.createElement('tr') - const heading = buildTableCell(name, 'th') + const resultSection = results[name]; + const tbody = document.createElement('tbody'); + tr = document.createElement('tr'); + const heading = buildTableCell(name, 'th'); // @ts-expect-error - colSpan is not defined in the type definition - heading.colSpan = 3 - tr.appendChild(heading) - tbody.appendChild(tr) + heading.colSpan = 3; + tr.appendChild(heading); + tbody.appendChild(tr); // @ts-expect-error - resultSection.forEach is not defined in the type definition resultSection.forEach((result) => { - const resultOut = JSON.stringify(result.result) - const expectedOut = JSON.stringify(result.expected) - tr = document.createElement('tr') - tr.appendChild(buildTableCell(result.name)) - tr.appendChild(buildTableCell(resultOut)) - tr.appendChild(buildTableCell(expectedOut)) - tr.classList.add(resultOut === expectedOut ? 'pass' : 'fail') - tbody.appendChild(tr) - }) - table.appendChild(tbody) + const resultOut = JSON.stringify(result.result); + const expectedOut = JSON.stringify(result.expected); + tr = document.createElement('tr'); + tr.appendChild(buildTableCell(result.name)); + tr.appendChild(buildTableCell(resultOut)); + tr.appendChild(buildTableCell(expectedOut)); + tr.classList.add(resultOut === expectedOut ? 'pass' : 'fail'); + tbody.appendChild(tr); + }); + table.appendChild(tbody); } - return table + return table; } -let isInAutomation = false -let isReadyPromiseResolve = null +let isInAutomation = false; +let isReadyPromiseResolve = null; const isReadyPromise = new Promise((resolve) => { - isReadyPromiseResolve = resolve -}) -const url = new URL(window.location.href) + isReadyPromiseResolve = resolve; +}); +const url = new URL(window.location.href); if (url.searchParams.get('automation')) { - isInAutomation = true + isInAutomation = true; window.addEventListener('content-scope-init-complete', () => { - isReadyPromiseResolve() - }) + isReadyPromiseResolve(); + }); } // @ts-expect-error - ongoingTests is not defined in the type definition -window.ongoingTests = [] +window.ongoingTests = []; // eslint-disable-next-line @typescript-eslint/no-unused-vars -function test (name, test) { +function test(name, test) { // @ts-expect-error - ongoingTests is not defined in the type definition - window.ongoingTests.push({ name, test }) + window.ongoingTests.push({ name, test }); } // eslint-disable-next-line @typescript-eslint/no-unused-vars -async function renderResults () { - const results = {} +async function renderResults() { + const results = {}; if (isInAutomation) { - await isReadyPromise + await isReadyPromise; } // @ts-expect-error - ongoingTests is not defined in the type definition for (const test of window.ongoingTests) { - const result = await test.test() - results[test.name] = result + const result = await test.test(); + results[test.name] = result; } // @ts-expect-error - buildResultTable is not defined in the type definition - document.body.appendChild(buildResultTable(results)) + document.body.appendChild(buildResultTable(results)); // @ts-expect-error - results is not defined in the type definition - window.results = results - window.dispatchEvent(new CustomEvent('results-ready', { detail: results })) + window.results = results; + window.dispatchEvent(new CustomEvent('results-ready', { detail: results })); } diff --git a/injected/integration-test/type-helpers.mjs b/injected/integration-test/type-helpers.mjs index 87091e402..549766c2e 100644 --- a/injected/integration-test/type-helpers.mjs +++ b/injected/integration-test/type-helpers.mjs @@ -1,4 +1,4 @@ -import { readFileSync } from 'node:fs' +import { readFileSync } from 'node:fs'; /** * Allows per-platform values. The 'platform' string is powered from globals.d.ts @@ -16,23 +16,23 @@ import { readFileSync } from 'node:fs' * @param {Partial, VariantFn>>} switchItems * @returns {ReturnType} */ -export function platform (name, switchItems) { +export function platform(name, switchItems) { if (name in switchItems) { - const fn = switchItems[name] + const fn = switchItems[name]; if (!fn) { - throw new Error('missing impl for that') + throw new Error('missing impl for that'); } - return fn() + return fn(); } - throw new Error('missing impl for that') + throw new Error('missing impl for that'); } export class Build { /** * @param {NonNullable} name */ - constructor (name) { - this.name = name + constructor(name) { + this.name = name; } /** @@ -40,43 +40,43 @@ export class Build { * @param {Partial, VariantFn>>} switchItems * @returns {ReturnType} */ - switch (switchItems) { + switch(switchItems) { if (this.name in switchItems) { - const fn = switchItems[this.name] + const fn = switchItems[this.name]; if (!fn) { - throw new Error('missing impl for that') + throw new Error('missing impl for that'); } - return fn() + return fn(); } - throw new Error('missing impl for that on platform: ' + this.name) + throw new Error('missing impl for that on platform: ' + this.name); } /** * * @returns string */ - get artifact () { + get artifact() { const path = this.switch({ windows: () => '../build/windows/contentScope.js', android: () => '../build/android/contentScope.js', apple: () => '../Sources/ContentScopeScripts/dist/contentScope.js', 'apple-isolated': () => '../Sources/ContentScopeScripts/dist/contentScopeIsolated.js', - 'android-autofill-password-import': () => '../build/android/autofillPasswordImport.js' - }) - return readFileSync(path, 'utf8') + 'android-autofill-password-import': () => '../build/android/autofillPasswordImport.js', + }); + return readFileSync(path, 'utf8'); } /** * @param {any} name * @returns {ImportMeta['injectName']} */ - static supported (name) { + static supported(name) { /** @type {ImportMeta['injectName'][]} */ - const items = ['apple', 'apple-isolated', 'windows', 'integration', 'android', 'android-autofill-password-import'] + const items = ['apple', 'apple-isolated', 'windows', 'integration', 'android', 'android-autofill-password-import']; if (items.includes(name)) { - return name + return name; } - return undefined + return undefined; } } @@ -85,21 +85,21 @@ export class PlatformInfo { * @param {object} params * @param {ImportMeta['platform']} params.name */ - constructor (params) { - this.name = params.name + constructor(params) { + this.name = params.name; } /** * @param {any} name * @returns {ImportMeta['platform']} */ - static supported (name) { + static supported(name) { /** @type {ImportMeta['platform'][]} */ - const items = ['macos', 'ios', 'windows', 'android'] + const items = ['macos', 'ios', 'windows', 'android']; if (items.includes(name)) { - return name + return name; } - return undefined + return undefined; } } @@ -110,26 +110,26 @@ export class PlatformInfo { * @param config * @returns {{build: Build; platformInfo: PlatformInfo}} */ -export function perPlatform (config) { +export function perPlatform(config) { // Read the configuration object to determine which platform we're testing against if (!('injectName' in config) || typeof config.injectName !== 'string') { - throw new Error('unsupported project - missing `use.injectName`') + throw new Error('unsupported project - missing `use.injectName`'); } if (!('platform' in config) || typeof config.platform !== 'string') { - throw new Error('unsupported project - missing `use.platform`') + throw new Error('unsupported project - missing `use.platform`'); } - const name = Build.supported(config.injectName) + const name = Build.supported(config.injectName); if (name) { - const build = new Build(name) - const platform = PlatformInfo.supported(config.platform) + const build = new Build(name); + const platform = PlatformInfo.supported(config.platform); if (platform) { - const platformInfo = new PlatformInfo({ name: platform }) - return { build, platformInfo } + const platformInfo = new PlatformInfo({ name: platform }); + return { build, platformInfo }; } } // If we get here, it's a mis-configuration - throw new Error('unreachable') + throw new Error('unreachable'); } diff --git a/injected/integration-test/utils.spec.js b/injected/integration-test/utils.spec.js index 1fb98250b..e04cdbafe 100644 --- a/injected/integration-test/utils.spec.js +++ b/injected/integration-test/utils.spec.js @@ -1,23 +1,23 @@ /** * Tests for utils */ -import { test as base, expect } from '@playwright/test' -import { gotoAndWait, testContextForExtension } from './helpers/harness.js' +import { test as base, expect } from '@playwright/test'; +import { gotoAndWait, testContextForExtension } from './helpers/harness.js'; -const test = testContextForExtension(base) +const test = testContextForExtension(base); test.describe('Ensure utils behave as expected', () => { test('should toString DDGProxy correctly', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { platform: { name: 'extension' } }) - const toStringResult = await page.evaluate('HTMLCanvasElement.prototype.getContext.toString()') - expect(toStringResult).toEqual('function getContext() { [native code] }') + await gotoAndWait(page, '/blank.html', { platform: { name: 'extension' } }); + const toStringResult = await page.evaluate('HTMLCanvasElement.prototype.getContext.toString()'); + expect(toStringResult).toEqual('function getContext() { [native code] }'); - const toStringToStringResult = await page.evaluate('HTMLCanvasElement.prototype.getContext.toString.toString()') - expect(toStringToStringResult).toEqual('function toString() { [native code] }') + const toStringToStringResult = await page.evaluate('HTMLCanvasElement.prototype.getContext.toString.toString()'); + expect(toStringToStringResult).toEqual('function toString() { [native code] }'); /* TOFIX: This is failing because the toString() call is being proxied and the result is not what we expect const callToStringToStringResult = await page.evaluate('String.toString.call(HTMLCanvasElement.prototype.getContext.toString)') expect(callToStringToStringResult).toEqual('function toString() { [native code] }') */ - }) -}) + }); +}); diff --git a/injected/integration-test/web-compat-android.spec.js b/injected/integration-test/web-compat-android.spec.js index bb9fab14f..5ce753a82 100644 --- a/injected/integration-test/web-compat-android.spec.js +++ b/injected/integration-test/web-compat-android.spec.js @@ -1,136 +1,148 @@ -import { gotoAndWait } from './helpers/harness.js' -import { test, expect } from '@playwright/test' +import { gotoAndWait } from './helpers/harness.js'; +import { test, expect } from '@playwright/test'; test.describe('Web Share API', () => { - function checkForCanShare () { - return 'canShare' in navigator + function checkForCanShare() { + return 'canShare' in navigator; } - function checkForShare () { - return 'share' in navigator + function checkForShare() { + return 'share' in navigator; } test.describe('disabled feature', () => { test('should not expose navigator.canShare() and navigator.share()', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }, null, 'script') - const noCanShare = await page.evaluate(checkForCanShare) - const noShare = await page.evaluate(checkForShare) + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }, null, 'script'); + const noCanShare = await page.evaluate(checkForCanShare); + const noShare = await page.evaluate(checkForShare); // Base implementation of the test env should not have it (it's only available on mobile) - expect(noCanShare).toEqual(false) - expect(noShare).toEqual(false) - }) - }) + expect(noCanShare).toEqual(false); + expect(noShare).toEqual(false); + }); + }); test.describe('disabled sub-feature', () => { test('should not expose navigator.canShare() and navigator.share()', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { - enabledFeatures: ['webCompat'] + await gotoAndWait( + page, + '/blank.html', + { + site: { + enabledFeatures: ['webCompat'], + }, + featureSettings: { + webCompat: { + // no webShare + }, + }, }, - featureSettings: { - webCompat: { - // no webShare - } - } - }, null, 'script') - const noCanShare = await page.evaluate(checkForCanShare) - const noShare = await page.evaluate(checkForShare) + null, + 'script', + ); + const noCanShare = await page.evaluate(checkForCanShare); + const noShare = await page.evaluate(checkForShare); // Base implementation of the test env should not have it (it's only available on mobile) - expect(noCanShare).toEqual(false) - expect(noShare).toEqual(false) - }) - }) + expect(noCanShare).toEqual(false); + expect(noShare).toEqual(false); + }); + }); test.describe('enabled feature', () => { - async function navigate (page) { - page.on('console', console.log) - await gotoAndWait(page, '/blank.html', { - site: { - enabledFeatures: ['webCompat'] + async function navigate(page) { + page.on('console', console.log); + await gotoAndWait( + page, + '/blank.html', + { + site: { + enabledFeatures: ['webCompat'], + }, + featureSettings: { + webCompat: { + webShare: 'enabled', + }, + }, }, - featureSettings: { - webCompat: { - webShare: 'enabled' - } - } - }, null, 'script') + null, + 'script', + ); } test('should expose navigator.canShare() and navigator.share() when enabled', async ({ page }) => { - await navigate(page) - const hasCanShare = await page.evaluate(checkForCanShare) - const hasShare = await page.evaluate(checkForShare) - expect(hasCanShare).toEqual(true) - expect(hasShare).toEqual(true) - }) + await navigate(page); + const hasCanShare = await page.evaluate(checkForCanShare); + const hasShare = await page.evaluate(checkForShare); + expect(hasCanShare).toEqual(true); + expect(hasShare).toEqual(true); + }); test.describe('navigator.canShare()', () => { test('should not let you share files', async ({ page }) => { - await navigate(page) + await navigate(page); const refuseFileShare = await page.evaluate(() => { - return navigator.canShare({ text: 'xxx', files: [] }) - }) - expect(refuseFileShare).toEqual(false) - }) + return navigator.canShare({ text: 'xxx', files: [] }); + }); + expect(refuseFileShare).toEqual(false); + }); test('should not let you share non-http urls', async ({ page }) => { - await navigate(page) + await navigate(page); const refuseShare = await page.evaluate(() => { - return navigator.canShare({ url: 'chrome://bla' }) - }) - expect(refuseShare).toEqual(false) - }) + return navigator.canShare({ url: 'chrome://bla' }); + }); + expect(refuseShare).toEqual(false); + }); test('should allow relative links', async ({ page }) => { - await navigate(page) + await navigate(page); const allowShare = await page.evaluate(() => { - return navigator.canShare({ url: 'bla' }) - }) - expect(allowShare).toEqual(true) - }) + return navigator.canShare({ url: 'bla' }); + }); + expect(allowShare).toEqual(true); + }); test('should support only the specific fields', async ({ page }) => { - await navigate(page) + await navigate(page); const refuseShare = await page.evaluate(() => { // eslint-disable-next-line // @ts-ignore intentionally malformed data - return navigator.canShare({ foo: 'bar' }) - }) - expect(refuseShare).toEqual(false) - }) + return navigator.canShare({ foo: 'bar' }); + }); + expect(refuseShare).toEqual(false); + }); test('should let you share stuff', async ({ page }) => { - await navigate(page) + await navigate(page); let canShare = await page.evaluate(() => { - return navigator.canShare({ url: 'http://example.com' }) - }) - expect(canShare).toEqual(true) + return navigator.canShare({ url: 'http://example.com' }); + }); + expect(canShare).toEqual(true); canShare = await page.evaluate(() => { - return navigator.canShare({ text: 'the grass was greener' }) - }) - expect(canShare).toEqual(true) + return navigator.canShare({ text: 'the grass was greener' }); + }); + expect(canShare).toEqual(true); canShare = await page.evaluate(() => { - return navigator.canShare({ title: 'the light was brighter' }) - }) - expect(canShare).toEqual(true) + return navigator.canShare({ title: 'the light was brighter' }); + }); + expect(canShare).toEqual(true); canShare = await page.evaluate(() => { - return navigator.canShare({ text: 'with friends surrounded', title: 'the nights of wonder' }) - }) - expect(canShare).toEqual(true) - }) - }) + return navigator.canShare({ text: 'with friends surrounded', title: 'the nights of wonder' }); + }); + expect(canShare).toEqual(true); + }); + }); test.describe('navigator.share()', () => { - async function beforeEach (page) { + async function beforeEach(page) { await page.evaluate(() => { - globalThis.shareReq = null + globalThis.shareReq = null; globalThis.cssMessaging.impl.request = (req) => { - globalThis.shareReq = req - return Promise.resolve({}) - } - }) + globalThis.shareReq = req; + return Promise.resolve({}); + }; + }); } test.describe('(no errors from Android)', () => { /** @@ -138,135 +150,143 @@ test.describe('Web Share API', () => { * @param {any} data * @return {Promise} */ - async function checkShare (page, data) { - const payload = `navigator.share(${JSON.stringify(data)})` + async function checkShare(page, data) { + const payload = `navigator.share(${JSON.stringify(data)})`; const result = await page.evaluate(payload).catch((e) => { - return { threw: e } - }) - console.log('check share', result) + return { threw: e }; + }); + console.log('check share', result); const message = await page.evaluate(() => { - console.log('did read?') - return globalThis.shareReq - }) - return { result, message } + console.log('did read?'); + return globalThis.shareReq; + }); + return { result, message }; } test('should let you share text', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { text: 'xxx' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { text: 'xxx' } }) - expect(result).toBeUndefined() - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { text: 'xxx' }); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { text: 'xxx' } }); + expect(result).toBeUndefined(); + }); test('should let you share url', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { url: 'http://example.com' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { url: 'http://example.com/' } }) - expect(result).toBeUndefined() - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { url: 'http://example.com' }); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { url: 'http://example.com/' } }); + expect(result).toBeUndefined(); + }); test('should let you share title alone', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { title: 'xxx' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { title: 'xxx', text: '' } }) - expect(result).toBeUndefined() - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { title: 'xxx' }); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { title: 'xxx', text: '' } }); + expect(result).toBeUndefined(); + }); test('should let you share title and text', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { title: 'xxx', text: 'yyy' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { title: 'xxx', text: 'yyy' } }) - expect(result).toBeUndefined() - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { title: 'xxx', text: 'yyy' }); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { title: 'xxx', text: 'yyy' } }); + expect(result).toBeUndefined(); + }); test('should let you share title and url', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { title: 'xxx', url: 'http://example.com' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { title: 'xxx', url: 'http://example.com/' } }) - expect(result).toBeUndefined() - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { title: 'xxx', url: 'http://example.com' }); + expect(message).toMatchObject({ + featureName: 'webCompat', + method: 'webShare', + params: { title: 'xxx', url: 'http://example.com/' }, + }); + expect(result).toBeUndefined(); + }); test('should combine text and url when both are present', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { text: 'xxx', url: 'http://example.com' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'webShare', params: { text: 'xxx http://example.com/' } }) - expect(result).toBeUndefined() - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { text: 'xxx', url: 'http://example.com' }); + expect(message).toMatchObject({ + featureName: 'webCompat', + method: 'webShare', + params: { text: 'xxx http://example.com/' }, + }); + expect(result).toBeUndefined(); + }); test('should throw when sharing files', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { title: 'title', files: [] }) - expect(message).toBeNull() - expect(result.threw.message).toContain('TypeError: Invalid share data') - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { title: 'title', files: [] }); + expect(message).toBeNull(); + expect(result.threw.message).toContain('TypeError: Invalid share data'); + }); test('should throw when sharing non-http urls', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { url: 'chrome://bla' }) - expect(message).toBeNull() - expect(result.threw.message).toContain('TypeError: Invalid share data') - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { url: 'chrome://bla' }); + expect(message).toBeNull(); + expect(result.threw.message).toContain('TypeError: Invalid share data'); + }); test('should handle relative urls', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { url: 'bla' }) - expect(message.params.url).toMatch(/^http:\/\/localhost:\d+\/bla$/) - expect(result).toBeUndefined() - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { url: 'bla' }); + expect(message.params.url).toMatch(/^http:\/\/localhost:\d+\/bla$/); + expect(result).toBeUndefined(); + }); test('should treat empty url as relative', async ({ page }) => { - await navigate(page) - await beforeEach(page) - const { result, message } = await checkShare(page, { url: '' }) - expect(message.params.url).toMatch(/^http:\/\/localhost:\d+\//) - expect(result).toBeUndefined() - }) - }) + await navigate(page); + await beforeEach(page); + const { result, message } = await checkShare(page, { url: '' }); + expect(message.params.url).toMatch(/^http:\/\/localhost:\d+\//); + expect(result).toBeUndefined(); + }); + }); test.describe('(handling errors from Android)', () => { test('should handle messaging error', async ({ page }) => { // page.on('console', (msg) => console.log(msg.type(), msg.text())) - await navigate(page) - await beforeEach(page) + await navigate(page); + await beforeEach(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = () => { - return Promise.reject(new Error('something wrong')) - } - }) + return Promise.reject(new Error('something wrong')); + }; + }); const result = await page.evaluate('navigator.share({ text: "xxx" })').catch((e) => { - return { threw: e } - }) + return { threw: e }; + }); // In page context, it should be a DOMException with name DataError, but page.evaluate() serializes everything in the message - expect(result.threw.message).toContain('DataError: something wrong') - }) + expect(result.threw.message).toContain('DataError: something wrong'); + }); test('should handle soft failures', async ({ page }) => { - await navigate(page) - await beforeEach(page) + await navigate(page); + await beforeEach(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = () => { - return Promise.resolve({ failure: { name: 'AbortError', message: 'some error message' } }) - } - }) + return Promise.resolve({ failure: { name: 'AbortError', message: 'some error message' } }); + }; + }); const result = await page.evaluate('navigator.share({ text: "xxx" })').catch((e) => { - return { threw: e } - }) + return { threw: e }; + }); // In page context, it should be a DOMException with name AbortError, but page.evaluate() serializes everything in the message - expect(result.threw.message).toContain('AbortError: some error message') - }) - }) - }) - }) -}) + expect(result.threw.message).toContain('AbortError: some error message'); + }); + }); + }); + }); +}); diff --git a/injected/integration-test/web-compat.spec.js b/injected/integration-test/web-compat.spec.js index 497633c7b..aabfc247f 100644 --- a/injected/integration-test/web-compat.spec.js +++ b/injected/integration-test/web-compat.spec.js @@ -1,269 +1,281 @@ -import { gotoAndWait, testContextForExtension } from './helpers/harness.js' -import { test as base, expect } from '@playwright/test' +import { gotoAndWait, testContextForExtension } from './helpers/harness.js'; +import { test as base, expect } from '@playwright/test'; -const test = testContextForExtension(base) +const test = testContextForExtension(base); test.describe('Ensure safari interface is injected', () => { test('should expose window.safari when enabled', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }) + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }); const noSafari = await page.evaluate(() => { - return 'safari' in window - }) - expect(noSafari).toEqual(false) + return 'safari' in window; + }); + expect(noSafari).toEqual(false); await gotoAndWait(page, '/blank.html', { site: { - enabledFeatures: ['webCompat'] + enabledFeatures: ['webCompat'], }, featureSettings: { webCompat: { - safariObject: 'enabled' - } - } - }) + safariObject: 'enabled', + }, + }, + }); const hasSafari = await page.evaluate(() => { - return 'safari' in window - }) - expect(hasSafari).toEqual(true) + return 'safari' in window; + }); + expect(hasSafari).toEqual(true); const pushNotificationToString = await page.evaluate(() => { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - return window.safari.pushNotification.toString() - }) - expect(pushNotificationToString).toEqual('[object SafariRemoteNotification]') + return window.safari.pushNotification.toString(); + }); + expect(pushNotificationToString).toEqual('[object SafariRemoteNotification]'); const pushNotificationPermission = await page.evaluate(() => { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - return window.safari.pushNotification.permission('test') - }) - expect(pushNotificationPermission.deviceToken).toEqual(null) - expect(pushNotificationPermission.permission).toEqual('denied') + return window.safari.pushNotification.permission('test'); + }); + expect(pushNotificationPermission.deviceToken).toEqual(null); + expect(pushNotificationPermission.permission).toEqual('denied'); const pushNotificationRequestPermissionThrow = await page.evaluate(() => { try { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - window.safari.pushNotification.requestPermission('test', 'test.com') + window.safari.pushNotification.requestPermission('test', 'test.com'); } catch (e) { - return e.message + return e.message; } - }) - expect(pushNotificationRequestPermissionThrow).toEqual('Invalid \'callback\' value passed to safari.pushNotification.requestPermission(). Expected a function.') + }); + expect(pushNotificationRequestPermissionThrow).toEqual( + "Invalid 'callback' value passed to safari.pushNotification.requestPermission(). Expected a function.", + ); const pushNotificationRequestPermission = await page.evaluate(() => { const response = new Promise((resolve) => { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f window.safari.pushNotification.requestPermission('test', 'test.com', {}, (data) => { - resolve(data) - }) - }) - return response - }) - expect(pushNotificationRequestPermission.deviceToken).toEqual(null) - expect(pushNotificationRequestPermission.permission).toEqual('denied') - }) -}) + resolve(data); + }); + }); + return response; + }); + expect(pushNotificationRequestPermission.deviceToken).toEqual(null); + expect(pushNotificationRequestPermission.permission).toEqual('denied'); + }); +}); test.describe('Ensure Notification interface is injected', () => { test('should expose window.Notification when enabled', async ({ page }) => { // Fake the Notification API not existing in this browser const removeNotificationScript = ` delete window.Notification - ` - function checkForNotification () { - return 'Notification' in window + `; + function checkForNotification() { + return 'Notification' in window; } - function checkObjectDescriptorSerializedValue () { - const descriptor = Object.getOwnPropertyDescriptor(window, 'Notification') - const out = {} + function checkObjectDescriptorSerializedValue() { + const descriptor = Object.getOwnPropertyDescriptor(window, 'Notification'); + const out = {}; for (const key in descriptor) { - out[key] = !!descriptor[key] + out[key] = !!descriptor[key]; } - return out + return out; } - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }) - const initialNotification = await page.evaluate(checkForNotification) + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }); + const initialNotification = await page.evaluate(checkForNotification); // Base implementation of the test env should have it. - expect(initialNotification).toEqual(true) - const initialDescriptorSerialization = await page.evaluate(checkObjectDescriptorSerializedValue) + expect(initialNotification).toEqual(true); + const initialDescriptorSerialization = await page.evaluate(checkObjectDescriptorSerializedValue); - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }, removeNotificationScript) + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }, removeNotificationScript); const noNotification = await page.evaluate(() => { - return 'Notification' in window - }) - expect(noNotification).toEqual(false) - - await gotoAndWait(page, '/blank.html', { - site: { - enabledFeatures: ['webCompat'] + return 'Notification' in window; + }); + expect(noNotification).toEqual(false); + + await gotoAndWait( + page, + '/blank.html', + { + site: { + enabledFeatures: ['webCompat'], + }, + featureSettings: { + webCompat: { + notification: 'enabled', + }, + }, }, - featureSettings: { - webCompat: { - notification: 'enabled' - } - } - }, removeNotificationScript) - const hasNotification = await page.evaluate(checkForNotification) - expect(hasNotification).toEqual(true) + removeNotificationScript, + ); + const hasNotification = await page.evaluate(checkForNotification); + expect(hasNotification).toEqual(true); - const modifiedDescriptorSerialization = await page.evaluate(checkObjectDescriptorSerializedValue) - expect(modifiedDescriptorSerialization).toEqual(initialDescriptorSerialization) + const modifiedDescriptorSerialization = await page.evaluate(checkObjectDescriptorSerializedValue); + expect(modifiedDescriptorSerialization).toEqual(initialDescriptorSerialization); const permissionDenied = await page.evaluate(() => { - return window.Notification.requestPermission() - }) - expect(permissionDenied).toEqual('denied') + return window.Notification.requestPermission(); + }); + expect(permissionDenied).toEqual('denied'); const permissionPropDenied = await page.evaluate(() => { - return window.Notification.permission - }) - expect(permissionPropDenied).toEqual('denied') + return window.Notification.permission; + }); + expect(permissionPropDenied).toEqual('denied'); const maxActionsPropDenied = await page.evaluate(() => { // @ts-expect-error - This is a property that should exist but experimental. - return window.Notification.maxActions - }) - expect(maxActionsPropDenied).toEqual(2) - }) -}) + return window.Notification.maxActions; + }); + expect(maxActionsPropDenied).toEqual(2); + }); +}); test.describe('Permissions API', () => { // Fake the Permission API not existing in this browser const removePermissionsScript = ` Object.defineProperty(window.navigator, 'permissions', { writable: true, value: undefined }) - ` + `; - function checkForPermissions () { - return !!window.navigator.permissions + function checkForPermissions() { + return !!window.navigator.permissions; } - function checkObjectDescriptorIsNotPresent () { - const descriptor = Object.getOwnPropertyDescriptor(window.navigator, 'permissions') - return descriptor === undefined + function checkObjectDescriptorIsNotPresent() { + const descriptor = Object.getOwnPropertyDescriptor(window.navigator, 'permissions'); + return descriptor === undefined; } test.describe('disabled feature', () => { test('should not expose permissions API', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }) - const initialPermissions = await page.evaluate(checkForPermissions) + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }); + const initialPermissions = await page.evaluate(checkForPermissions); // Base implementation of the test env should have it. - expect(initialPermissions).toEqual(true) - const initialDescriptorSerialization = await page.evaluate(checkObjectDescriptorIsNotPresent) - expect(initialDescriptorSerialization).toEqual(true) - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }, removePermissionsScript) - const noPermissions = await page.evaluate(checkForPermissions) - expect(noPermissions).toEqual(false) - }) - }) + expect(initialPermissions).toEqual(true); + const initialDescriptorSerialization = await page.evaluate(checkObjectDescriptorIsNotPresent); + expect(initialDescriptorSerialization).toEqual(true); + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }, removePermissionsScript); + const noPermissions = await page.evaluate(checkForPermissions); + expect(noPermissions).toEqual(false); + }); + }); test.describe('enabled feature', () => { /** * @param {import("@playwright/test").Page} page */ - async function before (page) { - await gotoAndWait(page, '/blank.html', { - site: { - enabledFeatures: ['webCompat'] - }, - featureSettings: { - webCompat: { - permissions: { - state: 'enabled', - supportedPermissions: { - geolocation: {}, - push: { - name: 'notifications' + async function before(page) { + await gotoAndWait( + page, + '/blank.html', + { + site: { + enabledFeatures: ['webCompat'], + }, + featureSettings: { + webCompat: { + permissions: { + state: 'enabled', + supportedPermissions: { + geolocation: {}, + push: { + name: 'notifications', + }, + camera: { + name: 'video_capture', + native: true, + }, }, - camera: { - name: 'video_capture', - native: true - } - } - } - } - } - }, removePermissionsScript) + }, + }, + }, + }, + removePermissionsScript, + ); } /** * @param {import("@playwright/test").Page} page * @param {any} name * @return {Promise<{result: any, message: *}>} */ - async function checkPermission (page, name) { - const payload = `window.navigator.permissions.query(${JSON.stringify({ name })})` + async function checkPermission(page, name) { + const payload = `window.navigator.permissions.query(${JSON.stringify({ name })})`; const result = await page.evaluate(payload).catch((e) => { - return { threw: e } - }) + return { threw: e }; + }); const message = await page.evaluate(() => { - return globalThis.shareReq - }) - return { result, message } + return globalThis.shareReq; + }); + return { result, message }; } test('should expose window.navigator.permissions when enabled', async ({ page }) => { - await before(page) - const hasPermissions = await page.evaluate(checkForPermissions) - expect(hasPermissions).toEqual(true) - const modifiedDescriptorSerialization = await page.evaluate(checkObjectDescriptorIsNotPresent) + await before(page); + const hasPermissions = await page.evaluate(checkForPermissions); + expect(hasPermissions).toEqual(true); + const modifiedDescriptorSerialization = await page.evaluate(checkObjectDescriptorIsNotPresent); // This fails in a test condition purely because we have to add a descriptor to modify the prop - expect(modifiedDescriptorSerialization).toEqual(false) - }) + expect(modifiedDescriptorSerialization).toEqual(false); + }); test('should throw error when permission not supported', async ({ page }) => { - await before(page) - const { result } = await checkPermission(page, 'notexistent') - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain('notexistent') - }) + await before(page); + const { result } = await checkPermission(page, 'notexistent'); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain('notexistent'); + }); test('should return prompt by default', async ({ page }) => { - await before(page) - const { result } = await checkPermission(page, 'geolocation') - expect(result).toMatchObject({ name: 'geolocation', state: 'prompt' }) - }) + await before(page); + const { result } = await checkPermission(page, 'geolocation'); + expect(result).toMatchObject({ name: 'geolocation', state: 'prompt' }); + }); test('should return updated name when configured', async ({ page }) => { - await before(page) - const { result } = await checkPermission(page, 'push') - expect(result).toMatchObject({ name: 'notifications', state: 'prompt' }) - }) + await before(page); + const { result } = await checkPermission(page, 'push'); + expect(result).toMatchObject({ name: 'notifications', state: 'prompt' }); + }); test('should propagate result from native when configured', async ({ page }) => { - await before(page) + await before(page); // Fake result from native await page.evaluate(() => { globalThis.cssMessaging.impl.request = (req) => { - globalThis.shareReq = req - return Promise.resolve({ state: 'granted' }) - } - }) - const { result, message } = await checkPermission(page, 'camera') - expect(result).toMatchObject({ name: 'video_capture', state: 'granted' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'permissionsQuery', params: { name: 'camera' } }) - }) + globalThis.shareReq = req; + return Promise.resolve({ state: 'granted' }); + }; + }); + const { result, message } = await checkPermission(page, 'camera'); + expect(result).toMatchObject({ name: 'video_capture', state: 'granted' }); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'permissionsQuery', params: { name: 'camera' } }); + }); test('should default to prompt when native sends unexpected response', async ({ page }) => { - await before(page) - page.on('console', msg => { - console.log(`PAGE LOG: ${msg.text()}`) - }) + await before(page); + page.on('console', (msg) => { + console.log(`PAGE LOG: ${msg.text()}`); + }); await page.evaluate(() => { globalThis.cssMessaging.impl.request = (message) => { - globalThis.shareReq = message - return Promise.resolve({ noState: 'xxx' }) - } - }) - const { result, message } = await checkPermission(page, 'camera') - expect(result).toMatchObject({ name: 'video_capture', state: 'prompt' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'permissionsQuery', params: { name: 'camera' } }) - }) + globalThis.shareReq = message; + return Promise.resolve({ noState: 'xxx' }); + }; + }); + const { result, message } = await checkPermission(page, 'camera'); + expect(result).toMatchObject({ name: 'video_capture', state: 'prompt' }); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'permissionsQuery', params: { name: 'camera' } }); + }); test('should default to prompt when native error occurs', async ({ page }) => { - await before(page) + await before(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = (message) => { - globalThis.shareReq = message - return Promise.reject(new Error('something wrong')) - } - }) - const { result, message } = await checkPermission(page, 'camera') - expect(result).toMatchObject({ name: 'video_capture', state: 'prompt' }) - expect(message).toMatchObject({ featureName: 'webCompat', method: 'permissionsQuery', params: { name: 'camera' } }) - }) - }) -}) + globalThis.shareReq = message; + return Promise.reject(new Error('something wrong')); + }; + }); + const { result, message } = await checkPermission(page, 'camera'); + expect(result).toMatchObject({ name: 'video_capture', state: 'prompt' }); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'permissionsQuery', params: { name: 'camera' } }); + }); + }); +}); test.describe('ScreenOrientation API', () => { test.describe('disabled feature', () => { @@ -272,300 +284,352 @@ test.describe('ScreenOrientation API', () => { * @param {any} orientation * @return {Promise} */ - async function checkLockThrows (page, orientation) { - const payload = `screen.orientation.lock(${JSON.stringify(orientation)})` + async function checkLockThrows(page, orientation) { + const payload = `screen.orientation.lock(${JSON.stringify(orientation)})`; const result = await page.evaluate(payload).catch((e) => { - return { threw: e } - }) - return result + return { threw: e }; + }); + return result; } test(' should not fix screenOrientation API', async ({ page }) => { // no screenLock setting - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: ['webCompat'] } }) - const result = await checkLockThrows(page, 'landscape') - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain('screen.orientation.lock() is not available on this device.') - }) - }) + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: ['webCompat'] } }); + const result = await checkLockThrows(page, 'landscape'); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain('screen.orientation.lock() is not available on this device.'); + }); + }); test.describe('enabled feature', () => { - async function beforeAll (page) { + async function beforeAll(page) { await gotoAndWait(page, '/blank.html', { site: { - enabledFeatures: ['webCompat'] + enabledFeatures: ['webCompat'], }, featureSettings: { webCompat: { - screenLock: 'enabled' - } - } - }) + screenLock: 'enabled', + }, + }, + }); } /** * @param {import("@playwright/test").Page} page * @param {any} orientation */ - async function checkLock (page, orientation) { - const payload = `screen.orientation.lock(${JSON.stringify(orientation)})` + async function checkLock(page, orientation) { + const payload = `screen.orientation.lock(${JSON.stringify(orientation)})`; const result = await page.evaluate(payload).catch((e) => { - return { threw: e } - }) + return { threw: e }; + }); const message = await page.evaluate(() => { - return globalThis.lockReq - }) - return { result, message } + return globalThis.lockReq; + }); + return { result, message }; } /** * @param {import("@playwright/test").Page} page */ - async function checkUnlock (page) { - const payload = 'screen.orientation.unlock()' + async function checkUnlock(page) { + const payload = 'screen.orientation.unlock()'; const result = await page.evaluate(payload).catch((e) => { - return { threw: e } - }) + return { threw: e }; + }); const message = await page.evaluate(() => { - return globalThis.lockReq - }) - return { result, message } + return globalThis.lockReq; + }); + return { result, message }; } test('should err out when orientation not provided', async ({ page }) => { - await beforeAll(page) - const { result } = await checkLock(page, undefined) - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain("Failed to execute 'lock' on 'ScreenOrientation': 1 argument required, but only 0 present") - }) + await beforeAll(page); + const { result } = await checkLock(page, undefined); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain( + "Failed to execute 'lock' on 'ScreenOrientation': 1 argument required, but only 0 present", + ); + }); test('should err out when orientation of unexpected type', async ({ page }) => { - await beforeAll(page) - const { result } = await checkLock(page, {}) - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain('not a valid enum value of type OrientationLockType') - }) + await beforeAll(page); + const { result } = await checkLock(page, {}); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain('not a valid enum value of type OrientationLockType'); + }); test('should err out when orientation of unexpected value', async ({ page }) => { - await beforeAll(page) - const { result } = await checkLock(page, 'xxx') - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain('not a valid enum value of type OrientationLockType') - }) + await beforeAll(page); + const { result } = await checkLock(page, 'xxx'); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain('not a valid enum value of type OrientationLockType'); + }); test('should propagate native TypeError', async ({ page }) => { - await beforeAll(page) + await beforeAll(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = () => { - return Promise.resolve({ failure: { name: 'TypeError', message: 'some error message' } }) - } - }) + return Promise.resolve({ failure: { name: 'TypeError', message: 'some error message' } }); + }; + }); - const { result } = await checkLock(page, 'landscape') - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain('some error message') - }) + const { result } = await checkLock(page, 'landscape'); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain('some error message'); + }); test('should propagate native InvalidStateError', async ({ page }) => { - await beforeAll(page) + await beforeAll(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = () => { - return Promise.resolve({ failure: { name: 'InvalidStateError', message: 'some error message' } }) - } - }) + return Promise.resolve({ failure: { name: 'InvalidStateError', message: 'some error message' } }); + }; + }); - const { result } = await checkLock(page, 'landscape') - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain('some error message') - }) + const { result } = await checkLock(page, 'landscape'); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain('some error message'); + }); test('should propagate native default error', async ({ page }) => { - await beforeAll(page) + await beforeAll(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = () => { - return Promise.resolve({ failure: { name: 'xxx', message: 'some error message' } }) - } - }) + return Promise.resolve({ failure: { name: 'xxx', message: 'some error message' } }); + }; + }); - const { result } = await checkLock(page, 'landscape') - expect(result.threw).not.toBeUndefined() - expect(result.threw.message).toContain('some error message') - }) + const { result } = await checkLock(page, 'landscape'); + expect(result.threw).not.toBeUndefined(); + expect(result.threw.message).toContain('some error message'); + }); test('should fix screenOrientation API', async ({ page }) => { - await beforeAll(page) + await beforeAll(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = (req) => { - globalThis.lockReq = req - return Promise.resolve({}) - } - }) + globalThis.lockReq = req; + return Promise.resolve({}); + }; + }); - const { result, message } = await checkLock(page, 'landscape') - expect(result).toBeUndefined() - expect(message).toMatchObject({ featureName: 'webCompat', method: 'screenLock', params: { orientation: 'landscape' } }) - }) + const { result, message } = await checkLock(page, 'landscape'); + expect(result).toBeUndefined(); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'screenLock', params: { orientation: 'landscape' } }); + }); test('should send message on unlock', async ({ page }) => { - await beforeAll(page) + await beforeAll(page); await page.evaluate(() => { globalThis.cssMessaging.impl.request = (req) => { - globalThis.lockReq = req - return Promise.resolve({}) - } - }) - - const { result, message } = await checkUnlock(page) - expect(result).toBeUndefined() - expect(message).toMatchObject({ featureName: 'webCompat', method: 'screenUnlock' }) - }) - }) -}) + globalThis.lockReq = req; + return Promise.resolve({}); + }; + }); + + const { result, message } = await checkUnlock(page); + expect(result).toBeUndefined(); + expect(message).toMatchObject({ featureName: 'webCompat', method: 'screenUnlock' }); + }); + }); +}); test.describe('Viewport fixes', () => { - function getViewportValue () { - return document.querySelector('meta[name="viewport"]')?.getAttribute('content') + function getViewportValue() { + return document.querySelector('meta[name="viewport"]')?.getAttribute('content'); } test('should not change viewport if disabled', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }, 'document.head.innerHTML += \'\'') - const initialViewportValue = await page.evaluate(getViewportValue) + await gotoAndWait( + page, + '/blank.html', + { site: { enabledFeatures: [] } }, + 'document.head.innerHTML += \'\'', + ); + const initialViewportValue = await page.evaluate(getViewportValue); // Base implementation of the test env should have it. - expect(initialViewportValue).toEqual('width=device-width') + expect(initialViewportValue).toEqual('width=device-width'); // We don't make a change if disabled - await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }) - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toBeUndefined() - }) + await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: [] } }); + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toBeUndefined(); + }); test('should respect forced zoom', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { enabledFeatures: ['webCompat'] }, - featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: false, - forcedZoomEnabled: true - }, 'document.head.innerHTML += \'\'') + await gotoAndWait( + page, + '/blank.html', + { + site: { enabledFeatures: ['webCompat'] }, + featureSettings: { webCompat: { viewportWidth: 'enabled' } }, + desktopModeEnabled: false, + forcedZoomEnabled: true, + }, + 'document.head.innerHTML += \'\'', + ); - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual('initial-scale=1, user-scalable=yes, maximum-scale=10, width=device-width') - }) + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual('initial-scale=1, user-scalable=yes, maximum-scale=10, width=device-width'); + }); test.describe('Desktop mode off', () => { test('should respect the forcedMobileValue config', async ({ page }) => { await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: ['webCompat'] }, featureSettings: { webCompat: { viewportWidth: { state: 'enabled', forcedMobileValue: 'bla, bla, bla' } } }, - desktopModeEnabled: false - }) - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual('bla, bla, bla') - }) + desktopModeEnabled: false, + }); + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual('bla, bla, bla'); + }); test('should force wide viewport if the meta tag is not present', async ({ page }) => { await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: ['webCompat'] }, featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: false - }) - const width = await page.evaluate('screen.width') - const expectedWidth = width < 1280 ? 980 : 1280 - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes`) - }) + desktopModeEnabled: false, + }); + const width = await page.evaluate('screen.width'); + const expectedWidth = width < 1280 ? 980 : 1280; + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes`); + }); test('should respect forced zoom', async ({ page }) => { await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: ['webCompat'] }, featureSettings: { webCompat: { viewportWidth: 'enabled' } }, desktopModeEnabled: false, - forcedZoomEnabled: true - }) - const width = await page.evaluate('screen.width') - const expectedWidth = width < 1280 ? 980 : 1280 - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual(`initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, maximum-scale=10, width=${expectedWidth}`) - }) + forcedZoomEnabled: true, + }); + const width = await page.evaluate('screen.width'); + const expectedWidth = width < 1280 ? 980 : 1280; + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual( + `initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, maximum-scale=10, width=${expectedWidth}`, + ); + }); test('should fix the WebView edge case', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { enabledFeatures: ['webCompat'] }, - featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: false - }, 'document.head.innerHTML += \'\'') - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual('width=device-width, initial-scale=1.00001, something-something') - }) + await gotoAndWait( + page, + '/blank.html', + { + site: { enabledFeatures: ['webCompat'] }, + featureSettings: { webCompat: { viewportWidth: 'enabled' } }, + desktopModeEnabled: false, + }, + 'document.head.innerHTML += \'\'', + ); + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual('width=device-width, initial-scale=1.00001, something-something'); + }); test('should ignore the character case in the viewport tag', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { enabledFeatures: ['webCompat'] }, - featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: false - }, 'document.head.innerHTML += \'\'') - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual('width=device-width, initIAL-scale=1.00001, something-something') - }) - }) + await gotoAndWait( + page, + '/blank.html', + { + site: { enabledFeatures: ['webCompat'] }, + featureSettings: { webCompat: { viewportWidth: 'enabled' } }, + desktopModeEnabled: false, + }, + 'document.head.innerHTML += \'\'', + ); + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual('width=device-width, initIAL-scale=1.00001, something-something'); + }); + }); test.describe('Desktop mode on', () => { test('should respect the forcedDesktopValue config', async ({ page }) => { await gotoAndWait(page, '/blank.html', { site: { enabledFeatures: ['webCompat'] }, featureSettings: { webCompat: { viewportWidth: { state: 'enabled', forcedDesktopValue: 'bla, bla, bla' } } }, - desktopModeEnabled: true - }) - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual('bla, bla, bla') - }) + desktopModeEnabled: true, + }); + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual('bla, bla, bla'); + }); test('should force wide viewport, ignoring the viewport tag', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { enabledFeatures: ['webCompat'] }, - featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: true - }, 'document.head.innerHTML += \'\'') - const width = await page.evaluate('screen.width') - const expectedWidth = width < 1280 ? 980 : 1280 - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`) - }) + await gotoAndWait( + page, + '/blank.html', + { + site: { enabledFeatures: ['webCompat'] }, + featureSettings: { webCompat: { viewportWidth: 'enabled' } }, + desktopModeEnabled: true, + }, + 'document.head.innerHTML += \'\'', + ); + const width = await page.evaluate('screen.width'); + const expectedWidth = width < 1280 ? 980 : 1280; + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual( + `width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`, + ); + }); test('should force wide viewport, ignoring the viewport tag 2', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { enabledFeatures: ['webCompat'] }, - featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: true - }, 'document.head.innerHTML += \'\'') - const width = await page.evaluate('screen.width') - const expectedWidth = width < 1280 ? 980 : 1280 - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`) - }) + await gotoAndWait( + page, + '/blank.html', + { + site: { enabledFeatures: ['webCompat'] }, + featureSettings: { webCompat: { viewportWidth: 'enabled' } }, + desktopModeEnabled: true, + }, + 'document.head.innerHTML += \'\'', + ); + const width = await page.evaluate('screen.width'); + const expectedWidth = width < 1280 ? 980 : 1280; + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual( + `width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`, + ); + }); test('should respect forced zoom', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { enabledFeatures: ['webCompat'] }, - featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: true, - forcedZoomEnabled: true - }, 'document.head.innerHTML += \'\'') - const width = await page.evaluate('screen.width') - const expectedWidth = width < 1280 ? 980 : 1280 - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual(`initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, maximum-scale=10, width=${expectedWidth}, something-something`) - }) + await gotoAndWait( + page, + '/blank.html', + { + site: { enabledFeatures: ['webCompat'] }, + featureSettings: { webCompat: { viewportWidth: 'enabled' } }, + desktopModeEnabled: true, + forcedZoomEnabled: true, + }, + 'document.head.innerHTML += \'\'', + ); + const width = await page.evaluate('screen.width'); + const expectedWidth = width < 1280 ? 980 : 1280; + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual( + `initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, maximum-scale=10, width=${expectedWidth}, something-something`, + ); + }); test('should ignore the character case in the viewport tag', async ({ page }) => { - await gotoAndWait(page, '/blank.html', { - site: { enabledFeatures: ['webCompat'] }, - featureSettings: { webCompat: { viewportWidth: 'enabled' } }, - desktopModeEnabled: true - }, 'document.head.innerHTML += \'\'') - const width = await page.evaluate('screen.width') - const expectedWidth = width < 1280 ? 980 : 1280 - const viewportValue = await page.evaluate(getViewportValue) - expect(viewportValue).toEqual(`width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`) - }) - }) -}) + await gotoAndWait( + page, + '/blank.html', + { + site: { enabledFeatures: ['webCompat'] }, + featureSettings: { webCompat: { viewportWidth: 'enabled' } }, + desktopModeEnabled: true, + }, + 'document.head.innerHTML += \'\'', + ); + const width = await page.evaluate('screen.width'); + const expectedWidth = width < 1280 ? 980 : 1280; + const viewportValue = await page.evaluate(getViewportValue); + expect(viewportValue).toEqual( + `width=${expectedWidth}, initial-scale=${(width / expectedWidth).toFixed(3)}, user-scalable=yes, something-something`, + ); + }); + }); +}); diff --git a/injected/integration-test/webcompat.spec.js b/injected/integration-test/webcompat.spec.js index 1a19a6970..60c33f675 100644 --- a/injected/integration-test/webcompat.spec.js +++ b/injected/integration-test/webcompat.spec.js @@ -1,69 +1,72 @@ -import { test, expect } from '@playwright/test' -import { readFileSync } from 'fs' -import { - mockWebkitMessaging, - wrapWebkitScripts -} from '@duckduckgo/messaging/lib/test-utils.mjs' -import { perPlatform } from './type-helpers.mjs' +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'fs'; +import { mockWebkitMessaging, wrapWebkitScripts } from '@duckduckgo/messaging/lib/test-utils.mjs'; +import { perPlatform } from './type-helpers.mjs'; test('web compat', async ({ page }, testInfo) => { - const webcompat = WebcompatSpec.create(page, testInfo) - await webcompat.enabled() - const results = await webcompat.collectResults() + const webcompat = WebcompatSpec.create(page, testInfo); + await webcompat.enabled(); + const results = await webcompat.collectResults(); expect(results).toMatchObject({ - 'webkit.messageHandlers - polyfill prevents throw': [{ - name: 'Error not thrown polyfil', - result: true, - expected: true - }], - 'webkit.messageHandlers - undefined should throw': [{ - name: 'undefined handler should throw', - result: true, - expected: true - }], - 'webkit.messageHandlers - reflected message': [{ - name: 'reflected message should pass through', - result: 'test', - expected: 'test' - }] - }) -}) + 'webkit.messageHandlers - polyfill prevents throw': [ + { + name: 'Error not thrown polyfil', + result: true, + expected: true, + }, + ], + 'webkit.messageHandlers - undefined should throw': [ + { + name: 'undefined handler should throw', + result: true, + expected: true, + }, + ], + 'webkit.messageHandlers - reflected message': [ + { + name: 'reflected message should pass through', + result: 'test', + expected: 'test', + }, + ], + }); +}); export class WebcompatSpec { - htmlPage = '/webcompat/pages/message-handlers.html' - config = './integration-test/test-pages/webcompat/config/message-handlers.json' + htmlPage = '/webcompat/pages/message-handlers.html'; + config = './integration-test/test-pages/webcompat/config/message-handlers.json'; /** * @param {import('@playwright/test').Page} page * @param {import('./type-helpers.mjs').Build} build * @param {import('./type-helpers.mjs').PlatformInfo} platform */ - constructor (page, build, platform) { - this.page = page - this.build = build - this.platform = platform + constructor(page, build, platform) { + this.page = page; + this.build = build; + this.platform = platform; page.on('console', (msg) => { - console.log(msg.type(), msg.text()) - }) + console.log(msg.type(), msg.text()); + }); } - async enabled () { - const config = JSON.parse(readFileSync(this.config, 'utf8')) - await this.setup({ config }) - await this.page.goto(this.htmlPage) + async enabled() { + const config = JSON.parse(readFileSync(this.config, 'utf8')); + await this.setup({ config }); + await this.page.goto(this.htmlPage); } - collectResults () { + collectResults() { return this.page.evaluate(() => { - return new Promise(resolve => { + return new Promise((resolve) => { // @ts-expect-error - this is added by the test framework - if (window.results) return resolve(window.results) + if (window.results) return resolve(window.results); window.addEventListener('results-ready', () => { // @ts-expect-error - this is added by the test framework - resolve(window.results) - }) - }) - }) + resolve(window.results); + }); + }); + }); } /** @@ -71,8 +74,8 @@ export class WebcompatSpec { * @param {Record} params.config * @return {Promise} */ - async setup (params) { - const { config } = params + async setup(params) { + const { config } = params; // read the built file from disk and do replacements const injectedJS = wrapWebkitScripts(this.build.artifact, { @@ -80,21 +83,21 @@ export class WebcompatSpec { $USER_UNPROTECTED_DOMAINS$: [], $USER_PREFERENCES$: { platform: { name: 'windows' }, - debug: true - } - }) + debug: true, + }, + }); await this.page.addInitScript(mockWebkitMessaging, { messagingContext: { env: 'development', context: 'contentScopeScripts', - featureName: 'n/a' + featureName: 'n/a', }, - responses: {} - }) + responses: {}, + }); // attach the JS - await this.page.addInitScript(injectedJS) + await this.page.addInitScript(injectedJS); } /** @@ -102,12 +105,9 @@ export class WebcompatSpec { * @param {import('@playwright/test').Page} page * @param {import('@playwright/test').TestInfo} testInfo */ - static create (page, testInfo) { + static create(page, testInfo) { // Read the configuration object to determine which platform we're testing against - const { - platformInfo, - build - } = perPlatform(testInfo.project.use) - return new WebcompatSpec(page, build, platformInfo) + const { platformInfo, build } = perPlatform(testInfo.project.use); + return new WebcompatSpec(page, build, platformInfo); } } diff --git a/injected/integration-test/windows-permissions.spec.js b/injected/integration-test/windows-permissions.spec.js index 8571cd040..90bc95d26 100644 --- a/injected/integration-test/windows-permissions.spec.js +++ b/injected/integration-test/windows-permissions.spec.js @@ -1,49 +1,49 @@ -import { test, expect } from '@playwright/test' -import { readFileSync } from 'fs' -import { mockWindowsMessaging, wrapWindowsScripts } from '@duckduckgo/messaging/lib/test-utils.mjs' -import { perPlatform } from './type-helpers.mjs' -import { windowsGlobalPolyfills } from './shared.mjs' +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'fs'; +import { mockWindowsMessaging, wrapWindowsScripts } from '@duckduckgo/messaging/lib/test-utils.mjs'; +import { perPlatform } from './type-helpers.mjs'; +import { windowsGlobalPolyfills } from './shared.mjs'; test('Windows Permissions Usage', async ({ page }, testInfo) => { - const perms = WindowsPermissionsSpec.create(page, testInfo) - await perms.enabled() + const perms = WindowsPermissionsSpec.create(page, testInfo); + await perms.enabled(); const results = await page.evaluate(() => { // @ts-expect-error - this is added by the test framework - return window.results['Disabled Windows Permissions'] - }) + return window.results['Disabled Windows Permissions']; + }); for (const result of results) { - expect(result.result).toEqual(result.expected) + expect(result.result).toEqual(result.expected); } -}) +}); export class WindowsPermissionsSpec { - htmlPage = '/permissions/index.html' - config = './integration-test/test-pages/permissions/config/permissions.json' + htmlPage = '/permissions/index.html'; + config = './integration-test/test-pages/permissions/config/permissions.json'; /** * @param {import("@playwright/test").Page} page * @param {import("./type-helpers.mjs").Build} build * @param {import("./type-helpers.mjs").PlatformInfo} platform */ - constructor (page, build, platform) { - this.page = page - this.build = build - this.platform = platform + constructor(page, build, platform) { + this.page = page; + this.build = build; + this.platform = platform; } - async enabled () { - await this.installPolyfills() - const config = JSON.parse(readFileSync(this.config, 'utf8')) - await this.setup({ config }) - await this.page.goto(this.htmlPage) + async enabled() { + await this.installPolyfills(); + const config = JSON.parse(readFileSync(this.config, 'utf8')); + await this.setup({ config }); + await this.page.goto(this.htmlPage); } /** * In CI, the global objects such as USB might not be installed on the * version of chromium running there. */ - async installPolyfills () { - await this.page.addInitScript(windowsGlobalPolyfills) + async installPolyfills() { + await this.page.addInitScript(windowsGlobalPolyfills); } /** @@ -51,8 +51,8 @@ export class WindowsPermissionsSpec { * @param {Record} params.config * @return {Promise} */ - async setup (params) { - const { config } = params + async setup(params) { + const { config } = params; // read the built file from disk and do replacements const injectedJS = wrapWindowsScripts(this.build.artifact, { @@ -60,21 +60,21 @@ export class WindowsPermissionsSpec { $USER_UNPROTECTED_DOMAINS$: [], $USER_PREFERENCES$: { platform: { name: 'windows' }, - debug: true - } - }) + debug: true, + }, + }); await this.page.addInitScript(mockWindowsMessaging, { messagingContext: { env: 'development', context: 'contentScopeScripts', - featureName: 'n/a' + featureName: 'n/a', }, - responses: {} - }) + responses: {}, + }); // attach the JS - await this.page.addInitScript(injectedJS) + await this.page.addInitScript(injectedJS); } /** @@ -82,9 +82,9 @@ export class WindowsPermissionsSpec { * @param {import("@playwright/test").Page} page * @param {import("@playwright/test").TestInfo} testInfo */ - static create (page, testInfo) { + static create(page, testInfo) { // Read the configuration object to determine which platform we're testing against - const { platformInfo, build } = perPlatform(testInfo.project.use) - return new WindowsPermissionsSpec(page, build, platformInfo) + const { platformInfo, build } = perPlatform(testInfo.project.use); + return new WindowsPermissionsSpec(page, build, platformInfo); } } diff --git a/injected/lib/sjcl.js b/injected/lib/sjcl.js index 9da0c95f8..02f49901c 100644 --- a/injected/lib/sjcl.js +++ b/injected/lib/sjcl.js @@ -1,517 +1,554 @@ // @ts-nocheck - export const sjcl = (() => { +export const sjcl = (() => { /** @fileOverview Javascript cryptography implementation. - * - * Crush to remove comments, shorten variable names and - * generally reduce transmission size. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -"use strict"; -/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */ -/*global document, window, escape, unescape, module, require, Uint32Array */ - -/** - * The Stanford Javascript Crypto Library, top-level namespace. - * @namespace - */ -var sjcl = { - /** - * Symmetric ciphers. - * @namespace - */ - cipher: {}, - - /** - * Hash functions. Right now only SHA256 is implemented. - * @namespace - */ - hash: {}, - - /** - * Key exchange functions. Right now only SRP is implemented. - * @namespace - */ - keyexchange: {}, - - /** - * Cipher modes of operation. - * @namespace - */ - mode: {}, - - /** - * Miscellaneous. HMAC and PBKDF2. - * @namespace - */ - misc: {}, - - /** - * Bit array encoders and decoders. - * @namespace - * - * @description - * The members of this namespace are functions which translate between - * SJCL's bitArrays and other objects (usually strings). Because it - * isn't always clear which direction is encoding and which is decoding, - * the method names are "fromBits" and "toBits". - */ - codec: {}, - - /** - * Exceptions. - * @namespace - */ - exception: { + * + * Crush to remove comments, shorten variable names and + * generally reduce transmission size. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + + 'use strict'; + /*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */ + /*global document, window, escape, unescape, module, require, Uint32Array */ + /** - * Ciphertext is corrupt. - * @constructor + * The Stanford Javascript Crypto Library, top-level namespace. + * @namespace */ - corrupt: function(message) { - this.toString = function() { return "CORRUPT: "+this.message; }; - this.message = message; - }, - + var sjcl = { + /** + * Symmetric ciphers. + * @namespace + */ + cipher: {}, + + /** + * Hash functions. Right now only SHA256 is implemented. + * @namespace + */ + hash: {}, + + /** + * Key exchange functions. Right now only SRP is implemented. + * @namespace + */ + keyexchange: {}, + + /** + * Cipher modes of operation. + * @namespace + */ + mode: {}, + + /** + * Miscellaneous. HMAC and PBKDF2. + * @namespace + */ + misc: {}, + + /** + * Bit array encoders and decoders. + * @namespace + * + * @description + * The members of this namespace are functions which translate between + * SJCL's bitArrays and other objects (usually strings). Because it + * isn't always clear which direction is encoding and which is decoding, + * the method names are "fromBits" and "toBits". + */ + codec: {}, + + /** + * Exceptions. + * @namespace + */ + exception: { + /** + * Ciphertext is corrupt. + * @constructor + */ + corrupt: function (message) { + this.toString = function () { + return 'CORRUPT: ' + this.message; + }; + this.message = message; + }, + + /** + * Invalid parameter. + * @constructor + */ + invalid: function (message) { + this.toString = function () { + return 'INVALID: ' + this.message; + }; + this.message = message; + }, + + /** + * Bug or missing feature in SJCL. + * @constructor + */ + bug: function (message) { + this.toString = function () { + return 'BUG: ' + this.message; + }; + this.message = message; + }, + + /** + * Something isn't ready. + * @constructor + */ + notReady: function (message) { + this.toString = function () { + return 'NOT READY: ' + this.message; + }; + this.message = message; + }, + }, + }; + /** @fileOverview Arrays of bits, encoded as arrays of Numbers. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + /** - * Invalid parameter. - * @constructor + * Arrays of bits, encoded as arrays of Numbers. + * @namespace + * @description + *

+ * These objects are the currency accepted by SJCL's crypto functions. + *

+ * + *

+ * Most of our crypto primitives operate on arrays of 4-byte words internally, + * but many of them can take arguments that are not a multiple of 4 bytes. + * This library encodes arrays of bits (whose size need not be a multiple of 8 + * bits) as arrays of 32-bit words. The bits are packed, big-endian, into an + * array of words, 32 bits at a time. Since the words are double-precision + * floating point numbers, they fit some extra data. We use this (in a private, + * possibly-changing manner) to encode the number of bits actually present + * in the last word of the array. + *

+ * + *

+ * Because bitwise ops clear this out-of-band data, these arrays can be passed + * to ciphers like AES which want arrays of words. + *

*/ - invalid: function(message) { - this.toString = function() { return "INVALID: "+this.message; }; - this.message = message; - }, - + sjcl.bitArray = { + /** + * Array slices in units of bits. + * @param {bitArray} a The array to slice. + * @param {Number} bstart The offset to the start of the slice, in bits. + * @param {Number} bend The offset to the end of the slice, in bits. If this is undefined, + * slice until the end of the array. + * @return {bitArray} The requested slice. + */ + bitSlice: function (a, bstart, bend) { + a = sjcl.bitArray._shiftRight(a.slice(bstart / 32), 32 - (bstart & 31)).slice(1); + return bend === undefined ? a : sjcl.bitArray.clamp(a, bend - bstart); + }, + + /** + * Extract a number packed into a bit array. + * @param {bitArray} a The array to slice. + * @param {Number} bstart The offset to the start of the slice, in bits. + * @param {Number} blength The length of the number to extract. + * @return {Number} The requested slice. + */ + extract: function (a, bstart, blength) { + // FIXME: this Math.floor is not necessary at all, but for some reason + // seems to suppress a bug in the Chromium JIT. + var x, + sh = Math.floor((-bstart - blength) & 31); + if (((bstart + blength - 1) ^ bstart) & -32) { + // it crosses a boundary + x = (a[(bstart / 32) | 0] << (32 - sh)) ^ (a[(bstart / 32 + 1) | 0] >>> sh); + } else { + // within a single word + x = a[(bstart / 32) | 0] >>> sh; + } + return x & ((1 << blength) - 1); + }, + + /** + * Concatenate two bit arrays. + * @param {bitArray} a1 The first array. + * @param {bitArray} a2 The second array. + * @return {bitArray} The concatenation of a1 and a2. + */ + concat: function (a1, a2) { + if (a1.length === 0 || a2.length === 0) { + return a1.concat(a2); + } + + var last = a1[a1.length - 1], + shift = sjcl.bitArray.getPartial(last); + if (shift === 32) { + return a1.concat(a2); + } else { + return sjcl.bitArray._shiftRight(a2, shift, last | 0, a1.slice(0, a1.length - 1)); + } + }, + + /** + * Find the length of an array of bits. + * @param {bitArray} a The array. + * @return {Number} The length of a, in bits. + */ + bitLength: function (a) { + var l = a.length, + x; + if (l === 0) { + return 0; + } + x = a[l - 1]; + return (l - 1) * 32 + sjcl.bitArray.getPartial(x); + }, + + /** + * Truncate an array. + * @param {bitArray} a The array. + * @param {Number} len The length to truncate to, in bits. + * @return {bitArray} A new array, truncated to len bits. + */ + clamp: function (a, len) { + if (a.length * 32 < len) { + return a; + } + a = a.slice(0, Math.ceil(len / 32)); + var l = a.length; + len = len & 31; + if (l > 0 && len) { + a[l - 1] = sjcl.bitArray.partial(len, a[l - 1] & (0x80000000 >> (len - 1)), 1); + } + return a; + }, + + /** + * Make a partial word for a bit array. + * @param {Number} len The number of bits in the word. + * @param {Number} x The bits. + * @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side. + * @return {Number} The partial word. + */ + partial: function (len, x, _end) { + if (len === 32) { + return x; + } + return (_end ? x | 0 : x << (32 - len)) + len * 0x10000000000; + }, + + /** + * Get the number of bits used by a partial word. + * @param {Number} x The partial word. + * @return {Number} The number of bits used by the partial word. + */ + getPartial: function (x) { + return Math.round(x / 0x10000000000) || 32; + }, + + /** + * Compare two arrays for equality in a predictable amount of time. + * @param {bitArray} a The first array. + * @param {bitArray} b The second array. + * @return {boolean} true if a == b; false otherwise. + */ + equal: function (a, b) { + if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) { + return false; + } + var x = 0, + i; + for (i = 0; i < a.length; i++) { + x |= a[i] ^ b[i]; + } + return x === 0; + }, + + /** Shift an array right. + * @param {bitArray} a The array to shift. + * @param {Number} shift The number of bits to shift. + * @param {Number} [carry=0] A byte to carry in + * @param {bitArray} [out=[]] An array to prepend to the output. + * @private + */ + _shiftRight: function (a, shift, carry, out) { + var i, + last2 = 0, + shift2; + if (out === undefined) { + out = []; + } + + for (; shift >= 32; shift -= 32) { + out.push(carry); + carry = 0; + } + if (shift === 0) { + return out.concat(a); + } + + for (i = 0; i < a.length; i++) { + out.push(carry | (a[i] >>> shift)); + carry = a[i] << (32 - shift); + } + last2 = a.length ? a[a.length - 1] : 0; + shift2 = sjcl.bitArray.getPartial(last2); + out.push(sjcl.bitArray.partial((shift + shift2) & 31, shift + shift2 > 32 ? carry : out.pop(), 1)); + return out; + }, + + /** xor a block of 4 words together. + * @private + */ + _xor4: function (x, y) { + return [x[0] ^ y[0], x[1] ^ y[1], x[2] ^ y[2], x[3] ^ y[3]]; + }, + + /** byteswap a word array inplace. + * (does not handle partial words) + * @param {sjcl.bitArray} a word array + * @return {sjcl.bitArray} byteswapped array + */ + byteswapM: function (a) { + var i, + v, + m = 0xff00; + for (i = 0; i < a.length; ++i) { + v = a[i]; + a[i] = (v >>> 24) | ((v >>> 8) & m) | ((v & m) << 8) | (v << 24); + } + return a; + }, + }; + /** @fileOverview Bit array codec implementations. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + /** - * Bug or missing feature in SJCL. - * @constructor + * UTF-8 strings + * @namespace + */ + sjcl.codec.utf8String = { + /** Convert from a bitArray to a UTF-8 string. */ + fromBits: function (arr) { + var out = '', + bl = sjcl.bitArray.bitLength(arr), + i, + tmp; + for (i = 0; i < bl / 8; i++) { + if ((i & 3) === 0) { + tmp = arr[i / 4]; + } + out += String.fromCharCode(((tmp >>> 8) >>> 8) >>> 8); + tmp <<= 8; + } + return decodeURIComponent(escape(out)); + }, + + /** Convert from a UTF-8 string to a bitArray. */ + toBits: function (str) { + str = unescape(encodeURIComponent(str)); + var out = [], + i, + tmp = 0; + for (i = 0; i < str.length; i++) { + tmp = (tmp << 8) | str.charCodeAt(i); + if ((i & 3) === 3) { + out.push(tmp); + tmp = 0; + } + } + if (i & 3) { + out.push(sjcl.bitArray.partial(8 * (i & 3), tmp)); + } + return out; + }, + }; + /** @fileOverview Bit array codec implementations. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + + /** + * Hexadecimal + * @namespace + */ + sjcl.codec.hex = { + /** Convert from a bitArray to a hex string. */ + fromBits: function (arr) { + var out = '', + i; + for (i = 0; i < arr.length; i++) { + out += ((arr[i] | 0) + 0xf00000000000).toString(16).substr(4); + } + return out.substr(0, sjcl.bitArray.bitLength(arr) / 4); //.replace(/(.{8})/g, "$1 "); + }, + /** Convert from a hex string to a bitArray. */ + toBits: function (str) { + var i, + out = [], + len; + str = str.replace(/\s|0x/g, ''); + len = str.length; + str = str + '00000000'; + for (i = 0; i < str.length; i += 8) { + out.push(parseInt(str.substr(i, 8), 16) ^ 0); + } + return sjcl.bitArray.clamp(out, len * 4); + }, + }; + + /** @fileOverview Javascript SHA-256 implementation. + * + * An older version of this implementation is available in the public + * domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh, + * Stanford University 2008-2010 and BSD-licensed for liability + * reasons. + * + * Special thanks to Aldo Cortesi for pointing out several bugs in + * this code. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh */ - bug: function(message) { - this.toString = function() { return "BUG: "+this.message; }; - this.message = message; - }, /** - * Something isn't ready. + * Context for a SHA-256 operation in progress. * @constructor */ - notReady: function(message) { - this.toString = function() { return "NOT READY: "+this.message; }; - this.message = message; - } - } -}; -/** @fileOverview Arrays of bits, encoded as arrays of Numbers. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * Arrays of bits, encoded as arrays of Numbers. - * @namespace - * @description - *

- * These objects are the currency accepted by SJCL's crypto functions. - *

- * - *

- * Most of our crypto primitives operate on arrays of 4-byte words internally, - * but many of them can take arguments that are not a multiple of 4 bytes. - * This library encodes arrays of bits (whose size need not be a multiple of 8 - * bits) as arrays of 32-bit words. The bits are packed, big-endian, into an - * array of words, 32 bits at a time. Since the words are double-precision - * floating point numbers, they fit some extra data. We use this (in a private, - * possibly-changing manner) to encode the number of bits actually present - * in the last word of the array. - *

- * - *

- * Because bitwise ops clear this out-of-band data, these arrays can be passed - * to ciphers like AES which want arrays of words. - *

- */ -sjcl.bitArray = { - /** - * Array slices in units of bits. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} bend The offset to the end of the slice, in bits. If this is undefined, - * slice until the end of the array. - * @return {bitArray} The requested slice. - */ - bitSlice: function (a, bstart, bend) { - a = sjcl.bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1); - return (bend === undefined) ? a : sjcl.bitArray.clamp(a, bend-bstart); - }, - - /** - * Extract a number packed into a bit array. - * @param {bitArray} a The array to slice. - * @param {Number} bstart The offset to the start of the slice, in bits. - * @param {Number} blength The length of the number to extract. - * @return {Number} The requested slice. - */ - extract: function(a, bstart, blength) { - // FIXME: this Math.floor is not necessary at all, but for some reason - // seems to suppress a bug in the Chromium JIT. - var x, sh = Math.floor((-bstart-blength) & 31); - if ((bstart + blength - 1 ^ bstart) & -32) { - // it crosses a boundary - x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh); - } else { - // within a single word - x = a[bstart/32|0] >>> sh; - } - return x & ((1< 0 && len) { - a[l-1] = sjcl.bitArray.partial(len, a[l-1] & 0x80000000 >> (len-1), 1); - } - return a; - }, - - /** - * Make a partial word for a bit array. - * @param {Number} len The number of bits in the word. - * @param {Number} x The bits. - * @param {Number} [_end=0] Pass 1 if x has already been shifted to the high side. - * @return {Number} The partial word. - */ - partial: function (len, x, _end) { - if (len === 32) { return x; } - return (_end ? x|0 : x << (32-len)) + len * 0x10000000000; - }, - - /** - * Get the number of bits used by a partial word. - * @param {Number} x The partial word. - * @return {Number} The number of bits used by the partial word. - */ - getPartial: function (x) { - return Math.round(x/0x10000000000) || 32; - }, - - /** - * Compare two arrays for equality in a predictable amount of time. - * @param {bitArray} a The first array. - * @param {bitArray} b The second array. - * @return {boolean} true if a == b; false otherwise. - */ - equal: function (a, b) { - if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) { - return false; - } - var x = 0, i; - for (i=0; i= 32; shift -= 32) { - out.push(carry); - carry = 0; - } - if (shift === 0) { - return out.concat(a); - } - - for (i=0; i>>shift); - carry = a[i] << (32-shift); - } - last2 = a.length ? a[a.length-1] : 0; - shift2 = sjcl.bitArray.getPartial(last2); - out.push(sjcl.bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1)); - return out; - }, - - /** xor a block of 4 words together. - * @private - */ - _xor4: function(x,y) { - return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]]; - }, - - /** byteswap a word array inplace. - * (does not handle partial words) - * @param {sjcl.bitArray} a word array - * @return {sjcl.bitArray} byteswapped array - */ - byteswapM: function(a) { - var i, v, m = 0xff00; - for (i = 0; i < a.length; ++i) { - v = a[i]; - a[i] = (v >>> 24) | ((v >>> 8) & m) | ((v & m) << 8) | (v << 24); - } - return a; - } -}; -/** @fileOverview Bit array codec implementations. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** - * UTF-8 strings - * @namespace - */ -sjcl.codec.utf8String = { - /** Convert from a bitArray to a UTF-8 string. */ - fromBits: function (arr) { - var out = "", bl = sjcl.bitArray.bitLength(arr), i, tmp; - for (i=0; i>> 8 >>> 8 >>> 8); - tmp <<= 8; - } - return decodeURIComponent(escape(out)); - }, - - /** Convert from a UTF-8 string to a bitArray. */ - toBits: function (str) { - str = unescape(encodeURIComponent(str)); - var out = [], i, tmp=0; - for (i=0; i 9007199254740991){ - throw new sjcl.exception.invalid("Cannot hash more than 2^53 - 1 bits"); - } - - if (typeof Uint32Array !== 'undefined') { - var c = new Uint32Array(b); - var j = 0; - for (i = 512+ol - ((512+ol) & 511); i <= nl; i+= 512) { - this._block(c.subarray(16 * j, 16 * (j+1))); - j += 1; - } - b.splice(0, 16 * j); - } else { - for (i = 512+ol - ((512+ol) & 511); i <= nl; i+= 512) { - this._block(b.splice(0,16)); - } - } - return this; - }, - - /** - * Complete hashing and output the hash value. - * @return {bitArray} The hash value, an array of 8 big-endian words. - */ - finalize:function () { - var i, b = this._buffer, h = this._h; - - // Round out and push the buffer - b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]); - - // Round out the buffer to a multiple of 16 words, less the 2 length words. - for (i = b.length + 2; i & 15; i++) { - b.push(0); - } - - // append the length - b.push(Math.floor(this._length / 0x100000000)); - b.push(this._length | 0); - - while (b.length) { - this._block(b.splice(0,16)); - } - - this.reset(); - return h; - }, - - /** - * The SHA-256 initialization vector, to be precomputed. - * @private - */ - _init:[], - /* + sjcl.hash.sha256 = function (hash) { + if (!this._key[0]) { + this._precompute(); + } + if (hash) { + this._h = hash._h.slice(0); + this._buffer = hash._buffer.slice(0); + this._length = hash._length; + } else { + this.reset(); + } + }; + + /** + * Hash a string or an array of words. + * @static + * @param {bitArray|String} data the data to hash. + * @return {bitArray} The hash value, an array of 16 big-endian words. + */ + sjcl.hash.sha256.hash = function (data) { + return new sjcl.hash.sha256().update(data).finalize(); + }; + + sjcl.hash.sha256.prototype = { + /** + * The hash's block size, in bits. + * @constant + */ + blockSize: 512, + + /** + * Reset the hash state. + * @return this + */ + reset: function () { + this._h = this._init.slice(0); + this._buffer = []; + this._length = 0; + return this; + }, + + /** + * Input several words to the hash. + * @param {bitArray|String} data the data to hash. + * @return this + */ + update: function (data) { + if (typeof data === 'string') { + data = sjcl.codec.utf8String.toBits(data); + } + var i, + b = (this._buffer = sjcl.bitArray.concat(this._buffer, data)), + ol = this._length, + nl = (this._length = ol + sjcl.bitArray.bitLength(data)); + if (nl > 9007199254740991) { + throw new sjcl.exception.invalid('Cannot hash more than 2^53 - 1 bits'); + } + + if (typeof Uint32Array !== 'undefined') { + var c = new Uint32Array(b); + var j = 0; + for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { + this._block(c.subarray(16 * j, 16 * (j + 1))); + j += 1; + } + b.splice(0, 16 * j); + } else { + for (i = 512 + ol - ((512 + ol) & 511); i <= nl; i += 512) { + this._block(b.splice(0, 16)); + } + } + return this; + }, + + /** + * Complete hashing and output the hash value. + * @return {bitArray} The hash value, an array of 8 big-endian words. + */ + finalize: function () { + var i, + b = this._buffer, + h = this._h; + + // Round out and push the buffer + b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1, 1)]); + + // Round out the buffer to a multiple of 16 words, less the 2 length words. + for (i = b.length + 2; i & 15; i++) { + b.push(0); + } + + // append the length + b.push(Math.floor(this._length / 0x100000000)); + b.push(this._length | 0); + + while (b.length) { + this._block(b.splice(0, 16)); + } + + this.reset(); + return h; + }, + + /** + * The SHA-256 initialization vector, to be precomputed. + * @private + */ + _init: [], + /* _init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19], */ - - /** - * The SHA-256 hash key, to be precomputed. - * @private - */ - _key:[], - /* + + /** + * The SHA-256 hash key, to be precomputed. + * @private + */ + _key: [], + /* _key: [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, @@ -523,154 +560,184 @@ sjcl.hash.sha256.prototype = { 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2], */ + /** + * Function to precompute _init and _key. + * @private + */ + _precompute: function () { + var i = 0, + prime = 2, + factor, + isPrime; + + function frac(x) { + return ((x - Math.floor(x)) * 0x100000000) | 0; + } + + for (; i < 64; prime++) { + isPrime = true; + for (factor = 2; factor * factor <= prime; factor++) { + if (prime % factor === 0) { + isPrime = false; + break; + } + } + if (isPrime) { + if (i < 8) { + this._init[i] = frac(Math.pow(prime, 1 / 2)); + } + this._key[i] = frac(Math.pow(prime, 1 / 3)); + i++; + } + } + }, + + /** + * Perform one cycle of SHA-256. + * @param {Uint32Array|bitArray} w one block of words. + * @private + */ + _block: function (w) { + var i, + tmp, + a, + b, + h = this._h, + k = this._key, + h0 = h[0], + h1 = h[1], + h2 = h[2], + h3 = h[3], + h4 = h[4], + h5 = h[5], + h6 = h[6], + h7 = h[7]; + + /* Rationale for placement of |0 : + * If a value can overflow is original 32 bits by a factor of more than a few + * million (2^23 ish), there is a possibility that it might overflow the + * 53-bit mantissa and lose precision. + * + * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that + * propagates around the loop, and on the hash state h[]. I don't believe + * that the clamps on h4 and on h0 are strictly necessary, but it's close + * (for h4 anyway), and better safe than sorry. + * + * The clamps on h[] are necessary for the output to be correct even in the + * common case and for short inputs. + */ + for (i = 0; i < 64; i++) { + // load up the input word for this round + if (i < 16) { + tmp = w[i]; + } else { + a = w[(i + 1) & 15]; + b = w[(i + 14) & 15]; + tmp = w[i & 15] = + (((a >>> 7) ^ (a >>> 18) ^ (a >>> 3) ^ (a << 25) ^ (a << 14)) + + ((b >>> 17) ^ (b >>> 19) ^ (b >>> 10) ^ (b << 15) ^ (b << 13)) + + w[i & 15] + + w[(i + 9) & 15]) | + 0; + } + + tmp = + tmp + + h7 + + ((h4 >>> 6) ^ (h4 >>> 11) ^ (h4 >>> 25) ^ (h4 << 26) ^ (h4 << 21) ^ (h4 << 7)) + + (h6 ^ (h4 & (h5 ^ h6))) + + k[i]; // | 0; + + // shift register + h7 = h6; + h6 = h5; + h5 = h4; + h4 = (h3 + tmp) | 0; + h3 = h2; + h2 = h1; + h1 = h0; + + h0 = + (tmp + + ((h1 & h2) ^ (h3 & (h1 ^ h2))) + + ((h1 >>> 2) ^ (h1 >>> 13) ^ (h1 >>> 22) ^ (h1 << 30) ^ (h1 << 19) ^ (h1 << 10))) | + 0; + } + + h[0] = (h[0] + h0) | 0; + h[1] = (h[1] + h1) | 0; + h[2] = (h[2] + h2) | 0; + h[3] = (h[3] + h3) | 0; + h[4] = (h[4] + h4) | 0; + h[5] = (h[5] + h5) | 0; + h[6] = (h[6] + h6) | 0; + h[7] = (h[7] + h7) | 0; + }, + }; + + /** @fileOverview HMAC implementation. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ - /** - * Function to precompute _init and _key. - * @private - */ - _precompute: function () { - var i = 0, prime = 2, factor, isPrime; - - function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; } - - for (; i<64; prime++) { - isPrime = true; - for (factor=2; factor*factor <= prime; factor++) { - if (prime % factor === 0) { - isPrime = false; - break; + /** HMAC with the specified hash function. + * @constructor + * @param {bitArray} key the key for HMAC. + * @param {Object} [Hash=sjcl.hash.sha256] The hash function to use. + */ + sjcl.misc.hmac = function (key, Hash) { + this._hash = Hash = Hash || sjcl.hash.sha256; + var exKey = [[], []], + i, + bs = Hash.prototype.blockSize / 32; + this._baseHash = [new Hash(), new Hash()]; + + if (key.length > bs) { + key = Hash.hash(key); } - } - if (isPrime) { - if (i<8) { - this._init[i] = frac(Math.pow(prime, 1/2)); + + for (i = 0; i < bs; i++) { + exKey[0][i] = key[i] ^ 0x36363636; + exKey[1][i] = key[i] ^ 0x5c5c5c5c; } - this._key[i] = frac(Math.pow(prime, 1/3)); - i++; - } - } - }, - - /** - * Perform one cycle of SHA-256. - * @param {Uint32Array|bitArray} w one block of words. - * @private - */ - _block:function (w) { - var i, tmp, a, b, - h = this._h, - k = this._key, - h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3], - h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7]; - - /* Rationale for placement of |0 : - * If a value can overflow is original 32 bits by a factor of more than a few - * million (2^23 ish), there is a possibility that it might overflow the - * 53-bit mantissa and lose precision. - * - * To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that - * propagates around the loop, and on the hash state h[]. I don't believe - * that the clamps on h4 and on h0 are strictly necessary, but it's close - * (for h4 anyway), and better safe than sorry. - * - * The clamps on h[] are necessary for the output to be correct even in the - * common case and for short inputs. + + this._baseHash[0].update(exKey[0]); + this._baseHash[1].update(exKey[1]); + this._resultHash = new Hash(this._baseHash[0]); + }; + + /** HMAC with the specified hash function. Also called encrypt since it's a prf. + * @param {bitArray|String} data The data to mac. */ - for (i=0; i<64; i++) { - // load up the input word for this round - if (i<16) { - tmp = w[i]; - } else { - a = w[(i+1 ) & 15]; - b = w[(i+14) & 15]; - tmp = w[i&15] = ((a>>>7 ^ a>>>18 ^ a>>>3 ^ a<<25 ^ a<<14) + - (b>>>17 ^ b>>>19 ^ b>>>10 ^ b<<15 ^ b<<13) + - w[i&15] + w[(i+9) & 15]) | 0; - } - - tmp = (tmp + h7 + (h4>>>6 ^ h4>>>11 ^ h4>>>25 ^ h4<<26 ^ h4<<21 ^ h4<<7) + (h6 ^ h4&(h5^h6)) + k[i]); // | 0; - - // shift register - h7 = h6; h6 = h5; h5 = h4; - h4 = h3 + tmp | 0; - h3 = h2; h2 = h1; h1 = h0; - - h0 = (tmp + ((h1&h2) ^ (h3&(h1^h2))) + (h1>>>2 ^ h1>>>13 ^ h1>>>22 ^ h1<<30 ^ h1<<19 ^ h1<<10)) | 0; - } - - h[0] = h[0]+h0 | 0; - h[1] = h[1]+h1 | 0; - h[2] = h[2]+h2 | 0; - h[3] = h[3]+h3 | 0; - h[4] = h[4]+h4 | 0; - h[5] = h[5]+h5 | 0; - h[6] = h[6]+h6 | 0; - h[7] = h[7]+h7 | 0; - } -}; - - -/** @fileOverview HMAC implementation. - * - * @author Emily Stark - * @author Mike Hamburg - * @author Dan Boneh - */ - -/** HMAC with the specified hash function. - * @constructor - * @param {bitArray} key the key for HMAC. - * @param {Object} [Hash=sjcl.hash.sha256] The hash function to use. - */ -sjcl.misc.hmac = function (key, Hash) { - this._hash = Hash = Hash || sjcl.hash.sha256; - var exKey = [[],[]], i, - bs = Hash.prototype.blockSize / 32; - this._baseHash = [new Hash(), new Hash()]; - - if (key.length > bs) { - key = Hash.hash(key); - } - - for (i=0; i !f.startsWith('.')) +function bundle(localesRoot) { + const locales = {}; + const localeDirs = readdirSync(localesRoot).filter((f) => !f.startsWith('.')); for (const locale of localeDirs) { - locales[locale] = {} - const dir = join(localesRoot, locale) - const files = readdirSync(dir) + locales[locale] = {}; + const dir = join(localesRoot, locale); + const files = readdirSync(dir); for (const file of files) { - const localeJSON = readFileSync(join(dir, file)) + const localeJSON = readFileSync(join(dir, file)); // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f - const stringObj = JSON.parse(localeJSON) - locales[locale][file] = {} + const stringObj = JSON.parse(localeJSON); + locales[locale][file] = {}; for (const [key, value] of Object.entries(stringObj)) { if (key !== 'smartling') { - locales[locale][file][key] = value.title + locales[locale][file][key] = value.title; } } } } - return 'export default `' + - JSON.stringify(locales).replace('`', '\\`') + - '`' + return 'export default `' + JSON.stringify(locales).replace('`', '\\`') + '`'; } const jobs = { 'src/locales/click-to-load': '../build/locales/ctl-locales.js', - 'src/locales/duckplayer': '../build/locales/duckplayer-locales.js' -} + 'src/locales/duckplayer': '../build/locales/duckplayer-locales.js', +}; for (const [dir, output] of Object.entries(jobs)) { - const bundled = bundle(dir) - write([output], bundled) + const bundled = bundle(dir); + write([output], bundled); } diff --git a/injected/scripts/bundleTrackers.mjs b/injected/scripts/bundleTrackers.mjs index 29a9a030c..91962b1f2 100644 --- a/injected/scripts/bundleTrackers.mjs +++ b/injected/scripts/bundleTrackers.mjs @@ -1,33 +1,33 @@ // eslint-disable-next-line no-redeclare -import fetch from 'node-fetch' -import { parseArgs, write } from '../../scripts/script-utils.js' +import fetch from 'node-fetch'; +import { parseArgs, write } from '../../scripts/script-utils.js'; -const tdsUrl = 'https://staticcdn.duckduckgo.com/trackerblocking/v4/tds.json' -const resp = await fetch(tdsUrl) -const tds = await resp.json() +const tdsUrl = 'https://staticcdn.duckduckgo.com/trackerblocking/v4/tds.json'; +const resp = await fetch(tdsUrl); +const tds = await resp.json(); // Build a trie of tracker domains, starting with the broadest subdomain. Leaves are set to 1 to indicate success // i.e. lookup['com']['example'] === 1 if example.com is a tracker domain -const trackerLookupTrie = {} -function insert (domainParts, node) { +const trackerLookupTrie = {}; +function insert(domainParts, node) { if (domainParts.length === 1) { - node[domainParts[0]] = 1 + node[domainParts[0]] = 1; } else if (node[domainParts[0]]) { - insert(domainParts.slice(1), node[domainParts[0]]) + insert(domainParts.slice(1), node[domainParts[0]]); } else { - node[domainParts[0]] = {} - insert(domainParts.slice(1), node[domainParts[0]]) + node[domainParts[0]] = {}; + insert(domainParts.slice(1), node[domainParts[0]]); } } Object.keys(tds.trackers).forEach((tracker) => { - insert(tracker.split('.').reverse(), trackerLookupTrie) -}) + insert(tracker.split('.').reverse(), trackerLookupTrie); +}); -const outputString = JSON.stringify(trackerLookupTrie) -const args = parseArgs(process.argv.slice(2), []) +const outputString = JSON.stringify(trackerLookupTrie); +const args = parseArgs(process.argv.slice(2), []); if (args.output) { - write([args.output], outputString) + write([args.output], outputString); } else { // Used by the extension code - console.log(outputString) + console.log(outputString); } diff --git a/injected/scripts/entry-points.js b/injected/scripts/entry-points.js index b10982c18..2335f0e4a 100644 --- a/injected/scripts/entry-points.js +++ b/injected/scripts/entry-points.js @@ -1,9 +1,9 @@ -import { postProcess, rollupScript } from './utils/build.js' -import { parseArgs, write } from '../../scripts/script-utils.js' -import { camelcase } from '../src/utils.js' +import { postProcess, rollupScript } from './utils/build.js'; +import { parseArgs, write } from '../../scripts/script-utils.js'; +import { camelcase } from '../src/utils.js'; -const contentScopePath = 'src/content-scope-features.js' -const contentScopeName = 'contentScopeFeatures' +const contentScopePath = 'src/content-scope-features.js'; +const contentScopeName = 'contentScopeFeatures'; /** * @typedef Build @@ -18,102 +18,102 @@ const contentScopeName = 'contentScopeFeatures' const builds = { firefox: { input: 'entry-points/mozilla.js', - output: ['../build/firefox/inject.js'] + output: ['../build/firefox/inject.js'], }, apple: { input: 'entry-points/apple.js', postProcess: true, - output: ['../Sources/ContentScopeScripts/dist/contentScope.js'] + output: ['../Sources/ContentScopeScripts/dist/contentScope.js'], }, 'apple-isolated': { input: 'entry-points/apple.js', - output: ['../Sources/ContentScopeScripts/dist/contentScopeIsolated.js'] + output: ['../Sources/ContentScopeScripts/dist/contentScopeIsolated.js'], }, android: { input: 'entry-points/android.js', - output: ['../build/android/contentScope.js'] + output: ['../build/android/contentScope.js'], }, 'android-autofill-password-import': { input: 'entry-points/android', - output: ['../build/android/autofillPasswordImport.js'] + output: ['../build/android/autofillPasswordImport.js'], }, windows: { input: 'entry-points/windows.js', - output: ['../build/windows/contentScope.js'] + output: ['../build/windows/contentScope.js'], }, integration: { input: 'entry-points/integration.js', output: [ '../build/integration/contentScope.js', 'integration-test/extension/contentScope.js', - 'integration-test/test-pages/build/contentScope.js' - ] + 'integration-test/test-pages/build/contentScope.js', + ], }, 'chrome-mv3': { input: 'entry-points/chrome-mv3.js', - output: ['../build/chrome-mv3/inject.js'] + output: ['../build/chrome-mv3/inject.js'], }, chrome: { input: 'entry-points/chrome.js', - output: ['../build/chrome/inject.js'] - } -} + output: ['../build/chrome/inject.js'], + }, +}; -async function initOther (injectScriptPath, platformName) { - const supportsMozProxies = platformName === 'firefox' - const identName = `inject${camelcase(platformName)}` +async function initOther(injectScriptPath, platformName) { + const supportsMozProxies = platformName === 'firefox'; + const identName = `inject${camelcase(platformName)}`; const injectScript = await rollupScript({ scriptPath: injectScriptPath, name: identName, supportsMozProxies, - platform: platformName - }) - const outputScript = injectScript - return outputScript + platform: platformName, + }); + const outputScript = injectScript; + return outputScript; } /** * @param {string} entry * @param {string} platformName */ -async function initChrome (entry, platformName) { - const replaceString = '/* global contentScopeFeatures */' - const injectScript = await rollupScript({ scriptPath: entry, platform: platformName }) +async function initChrome(entry, platformName) { + const replaceString = '/* global contentScopeFeatures */'; + const injectScript = await rollupScript({ scriptPath: entry, platform: platformName }); const contentScope = await rollupScript({ scriptPath: contentScopePath, name: contentScopeName, - platform: platformName - }) + platform: platformName, + }); // Encode in URI format to prevent breakage (we could choose to just escape ` instead) // NB: .replace(/\r\n/g, "\n") is needed because in Windows rollup generates CRLF line endings - const encodedString = encodeURI(contentScope.toString().replace(/\r\n/g, '\n')) - const outputScript = injectScript.toString().replace(replaceString, '${decodeURI("' + encodedString + '")}') - return outputScript + const encodedString = encodeURI(contentScope.toString().replace(/\r\n/g, '\n')); + const outputScript = injectScript.toString().replace(replaceString, '${decodeURI("' + encodedString + '")}'); + return outputScript; } -async function init () { +async function init() { // verify the input - const requiredFields = ['platform'] - const args = parseArgs(process.argv.slice(2), requiredFields) - const build = builds[args.platform] + const requiredFields = ['platform']; + const args = parseArgs(process.argv.slice(2), requiredFields); + const build = builds[args.platform]; if (!build) { - throw new Error('unsupported platform: ' + args.platform) + throw new Error('unsupported platform: ' + args.platform); } - let output + let output; if (args.platform === 'chrome') { - output = await initChrome(build.input, args.platform) + output = await initChrome(build.input, args.platform); } else { - output = await initOther(build.input, args.platform) + output = await initOther(build.input, args.platform); if (build.postProcess) { - const processResult = await postProcess(output) - output = processResult.code + const processResult = await postProcess(output); + output = processResult.code; } } // bundle and write the output - write([build.output], output) + write([build.output], output); } -init() +init(); diff --git a/injected/scripts/generate-har.js b/injected/scripts/generate-har.js index 402017ecf..e0dcfd769 100644 --- a/injected/scripts/generate-har.js +++ b/injected/scripts/generate-har.js @@ -1,21 +1,21 @@ -import { chromium } from '@playwright/test' +import { chromium } from '@playwright/test'; -const testPath = 'integration-test/data/har/duckduckgo.com/search.har' +const testPath = 'integration-test/data/har/duckduckgo.com/search.har'; -async function init () { - const browser = await chromium.launch() +async function init() { + const browser = await chromium.launch(); const context = await browser.newContext({ - recordHar: { path: testPath } - }) + recordHar: { path: testPath }, + }); - const page = await context.newPage() + const page = await context.newPage(); - await page.goto('https://duckduckgo.com/c-s-s-says-hello') - await page.waitForLoadState('networkidle') + await page.goto('https://duckduckgo.com/c-s-s-says-hello'); + await page.waitForLoadState('networkidle'); // Close context to ensure HAR is saved to disk. - await context.close() - await browser.close() + await context.close(); + await browser.close(); } -init() +init(); diff --git a/injected/scripts/generateSJCL.js b/injected/scripts/generateSJCL.js index 84ac0b17a..ba1e8ced9 100644 --- a/injected/scripts/generateSJCL.js +++ b/injected/scripts/generateSJCL.js @@ -1,29 +1,29 @@ -import { readFile, writeFile } from 'fs/promises' -import { existsSync } from 'fs' -import util from 'util' -import { exec as callbackExec } from 'child_process' -const exec = util.promisify(callbackExec) +import { readFile, writeFile } from 'fs/promises'; +import { existsSync } from 'fs'; +import util from 'util'; +import { exec as callbackExec } from 'child_process'; +const exec = util.promisify(callbackExec); -async function init () { +async function init() { if (!existsSync('node_modules/sjcl/')) { // If content scope scripts is installed as a module there is no need to copy sjcl - return + return; } if (process.platform === 'win32') { - console.log('skipping sjcl on windows') - return + console.log('skipping sjcl on windows'); + return; } - await exec('cd node_modules/sjcl/ && perl ./configure --no-export --compress=none --without-all --with-hmac --with-codecHex && make') - const sjclFileContents = await readFile('node_modules/sjcl/sjcl.js') + await exec('cd node_modules/sjcl/ && perl ./configure --no-export --compress=none --without-all --with-hmac --with-codecHex && make'); + const sjclFileContents = await readFile('node_modules/sjcl/sjcl.js'); // Reexport the file as es6 module format const contents = `// @ts-nocheck export const sjcl = (() => { ${sjclFileContents} return sjcl; - })()` - writeFile('lib/sjcl.js', contents) + })()`; + writeFile('lib/sjcl.js', contents); } -init() +init(); diff --git a/injected/scripts/server.mjs b/injected/scripts/server.mjs index a31a0c6d8..13d401af5 100644 --- a/injected/scripts/server.mjs +++ b/injected/scripts/server.mjs @@ -1,10 +1,10 @@ -import { createServer } from "http-server"; +import { createServer } from 'http-server'; const server = createServer({ root: process.env.SERVER_DIR }); server.listen(process.env.SERVER_PORT); server.server.on('listening', () => { - process.send?.({port: server.server.address().port}); -}) + process.send?.({ port: server.server.address().port }); +}); server.server.on('error', () => { - process.exit(1) -}) + process.exit(1); +}); diff --git a/injected/scripts/types.mjs b/injected/scripts/types.mjs index 9cc240190..9fc4d19ba 100644 --- a/injected/scripts/types.mjs +++ b/injected/scripts/types.mjs @@ -1,13 +1,13 @@ -import { cwd, isLaunchFile } from '../../scripts/script-utils.js' -import { dirname, join } from 'node:path' -import { createRequire } from 'node:module' -import { buildTypes } from "../../types-generator/build-types.mjs"; +import { cwd, isLaunchFile } from '../../scripts/script-utils.js'; +import { dirname, join } from 'node:path'; +import { createRequire } from 'node:module'; +import { buildTypes } from '../../types-generator/build-types.mjs'; -const injectRoot = join(cwd(import.meta.url), '..') +const injectRoot = join(cwd(import.meta.url), '..'); // eslint-disable-next-line no-redeclare const require = createRequire(import.meta.url); -const configBuilderRoot = dirname(require.resolve("config-builder")); +const configBuilderRoot = dirname(require.resolve('config-builder')); /** @type {Record} */ const injectSchemaMapping = { @@ -16,36 +16,36 @@ const injectSchemaMapping = { * - `schema` should be an absolute path to a valid JSON Schema document * - `types` should be an absolute path to the output file */ - "Webcompat Settings": { - schema: join(configBuilderRoot, "tests/schemas/webcompat-settings.json"), - types: join(injectRoot, "src/types/webcompat-settings.d.ts"), + 'Webcompat Settings': { + schema: join(configBuilderRoot, 'tests/schemas/webcompat-settings.json'), + types: join(injectRoot, 'src/types/webcompat-settings.d.ts'), // todo: fix this on windows. exclude: process.platform === 'win32', kind: 'settings', }, - "Duckplayer Settings": { - schema: join(configBuilderRoot, "tests/schemas/duckplayer-settings.json"), - types: join(injectRoot, "src/types/duckplayer-settings.d.ts"), + 'Duckplayer Settings': { + schema: join(configBuilderRoot, 'tests/schemas/duckplayer-settings.json'), + types: join(injectRoot, 'src/types/duckplayer-settings.d.ts'), // todo: fix this on windows. exclude: process.platform === 'win32', kind: 'settings', }, - "Schema Messages": { - schemaDir: join(injectRoot, "src/messages"), - typesDir: join(injectRoot, "src/types"), + 'Schema Messages': { + schemaDir: join(injectRoot, 'src/messages'), + typesDir: join(injectRoot, 'src/types'), // todo: fix this on windows. exclude: process.platform === 'win32', kind: 'messages', resolve: (dirname) => '../features/' + dirname + '.js', className: (topLevelType) => topLevelType.replace('Messages', ''), - } -} + }, +}; if (isLaunchFile(import.meta.url)) { buildTypes(injectSchemaMapping) // eslint-disable-next-line promise/prefer-await-to-then .catch((error) => { - console.error(error) - process.exit(1) - }) + console.error(error); + process.exit(1); + }); } diff --git a/injected/scripts/utils/build.js b/injected/scripts/utils/build.js index 8c13a488b..f1260ccbb 100644 --- a/injected/scripts/utils/build.js +++ b/injected/scripts/utils/build.js @@ -1,32 +1,32 @@ -import * as rollup from 'rollup' -import * as esbuild from 'esbuild' -import commonjs from '@rollup/plugin-commonjs' -import replace from '@rollup/plugin-replace' -import resolve from '@rollup/plugin-node-resolve' -import css from 'rollup-plugin-import-css' -import svg from 'rollup-plugin-svg-import' -import { platformSupport } from '../../src/features.js' -import { readFileSync } from 'fs' - -function prefixPlugin (prefixMessage) { +import * as rollup from 'rollup'; +import * as esbuild from 'esbuild'; +import commonjs from '@rollup/plugin-commonjs'; +import replace from '@rollup/plugin-replace'; +import resolve from '@rollup/plugin-node-resolve'; +import css from 'rollup-plugin-import-css'; +import svg from 'rollup-plugin-svg-import'; +import { platformSupport } from '../../src/features.js'; +import { readFileSync } from 'fs'; + +function prefixPlugin(prefixMessage) { return { name: 'prefix-plugin', - renderChunk (code) { - return `${prefixMessage}\n${code}` - } - } + renderChunk(code) { + return `${prefixMessage}\n${code}`; + }, + }; } -function suffixPlugin (suffixMessage) { +function suffixPlugin(suffixMessage) { return { name: 'suffix-plugin', - renderChunk (code) { - return `${code}\n${suffixMessage}` - } - } + renderChunk(code) { + return `${code}\n${suffixMessage}`; + }, + }; } -const prefixMessage = '/*! © DuckDuckGo ContentScopeScripts protections https://github.com/duckduckgo/content-scope-scripts/ */' +const prefixMessage = '/*! © DuckDuckGo ContentScopeScripts protections https://github.com/duckduckgo/content-scope-scripts/ */'; /** * @param {object} params @@ -37,29 +37,23 @@ const prefixMessage = '/*! © DuckDuckGo ContentScopeScripts protections https:/ * @param {boolean} [params.supportsMozProxies] * @return {Promise} */ -export async function rollupScript (params) { - const { - scriptPath, - platform, - name, - featureNames, - supportsMozProxies = false - } = params +export async function rollupScript(params) { + const { scriptPath, platform, name, featureNames, supportsMozProxies = false } = params; - const extensions = ['firefox', 'chrome', 'chrome-mv3'] - const isExtension = extensions.includes(platform) - let trackerLookup = '$TRACKER_LOOKUP$' + const extensions = ['firefox', 'chrome', 'chrome-mv3']; + const isExtension = extensions.includes(platform); + let trackerLookup = '$TRACKER_LOOKUP$'; if (!isExtension) { - const trackerLookupData = readFileSync('../build/tracker-lookup.json', 'utf8') - trackerLookup = trackerLookupData + const trackerLookupData = readFileSync('../build/tracker-lookup.json', 'utf8'); + trackerLookup = trackerLookupData; } - const suffixMessage = `/*# sourceURL=duckduckgo-privacy-protection.js?scope=${name} */` + const suffixMessage = `/*# sourceURL=duckduckgo-privacy-protection.js?scope=${name} */`; // The code is using a global, that we define here which means once tree shaken we get a browser specific output. - const mozProxies = supportsMozProxies + const mozProxies = supportsMozProxies; const plugins = [ css(), svg({ - stringify: true + stringify: true, }), loadFeatures(platform, featureNames), resolve(), @@ -71,20 +65,20 @@ export async function rollupScript (params) { mozProxies, 'import.meta.injectName': JSON.stringify(platform), // To be replaced by the extension, but prevents tree shaking - 'import.meta.trackerLookup': trackerLookup - } + 'import.meta.trackerLookup': trackerLookup, + }, }), - prefixPlugin(prefixMessage) - ] + prefixPlugin(prefixMessage), + ]; if (platform === 'firefox') { - plugins.push(suffixPlugin(suffixMessage)) + plugins.push(suffixPlugin(suffixMessage)); } const bundle = await rollup.rollup({ input: scriptPath, - plugins - }) + plugins, + }); const generated = await bundle.generate({ dir: 'build', @@ -92,10 +86,10 @@ export async function rollupScript (params) { inlineDynamicImports: true, name, // This if for seedrandom causing build issues - globals: { crypto: 'undefined' } - }) + globals: { crypto: 'undefined' }, + }); - return generated.output[0].code + return generated.output[0].code; } /** @@ -104,39 +98,37 @@ export async function rollupScript (params) { * @param {string} platform * @param {string[]} featureNames */ -function loadFeatures (platform, featureNames = platformSupport[platform]) { - const pluginId = 'ddg:platformFeatures' +function loadFeatures(platform, featureNames = platformSupport[platform]) { + const pluginId = 'ddg:platformFeatures'; return { name: pluginId, - resolveId (id) { - if (id === pluginId) return id - return null + resolveId(id) { + if (id === pluginId) return id; + return null; }, - load (id) { - if (id !== pluginId) return null + load(id) { + if (id !== pluginId) return null; // convert a list of feature names to const imports = featureNames.map((featureName) => { - const fileName = getFileName(featureName) - const path = `./src/features/${fileName}.js` - const ident = `ddg_feature_${featureName}` + const fileName = getFileName(featureName); + const path = `./src/features/${fileName}.js`; + const ident = `ddg_feature_${featureName}`; return { ident, - importPath: path - } - }) + importPath: path, + }; + }); - const importString = imports.map(imp => `import ${imp.ident} from ${JSON.stringify(imp.importPath)}`) - .join(';\n') + const importString = imports.map((imp) => `import ${imp.ident} from ${JSON.stringify(imp.importPath)}`).join(';\n'); - const exportsString = imports.map(imp => `${imp.ident}`) - .join(',\n ') + const exportsString = imports.map((imp) => `${imp.ident}`).join(',\n '); - const exportString = `export default {\n ${exportsString}\n}` + const exportString = `export default {\n ${exportsString}\n}`; - return [importString, exportString].join('\n') - } - } + return [importString, exportString].join('\n'); + }, + }; } /** @@ -145,8 +137,8 @@ function loadFeatures (platform, featureNames = platformSupport[platform]) { * @param {string} featureName * @return {string} */ -function getFileName (featureName) { - return featureName.replace(/([a-zA-Z])(?=[A-Z0-9])/g, '$1-').toLowerCase() +function getFileName(featureName) { + return featureName.replace(/([a-zA-Z])(?=[A-Z0-9])/g, '$1-').toLowerCase(); } /** @@ -160,6 +152,6 @@ function getFileName (featureName) { * @param {string} content * @return {Promise} */ -export function postProcess (content) { - return esbuild.transform(content, { target: 'es2021', format: 'iife' }) +export function postProcess(content) { + return esbuild.transform(content, { target: 'es2021', format: 'iife' }); } diff --git a/injected/src/canvas.js b/injected/src/canvas.js index 4ef364982..ccae78147 100644 --- a/injected/src/canvas.js +++ b/injected/src/canvas.js @@ -1,5 +1,5 @@ -import { getDataKeySync } from './crypto.js' -import Seedrandom from 'seedrandom' +import { getDataKeySync } from './crypto.js'; +import Seedrandom from 'seedrandom'; /** * @param {HTMLCanvasElement} canvas @@ -8,41 +8,41 @@ import Seedrandom from 'seedrandom' * @param {any} getImageDataProxy * @param {CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext} ctx? */ -export function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageDataProxy, ctx) { +export function computeOffScreenCanvas(canvas, domainKey, sessionKey, getImageDataProxy, ctx) { if (!ctx) { // @ts-expect-error - Type 'null' is not assignable to type 'CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext'. - ctx = canvas.getContext('2d') + ctx = canvas.getContext('2d'); } // Make a off-screen canvas and put the data there - const offScreenCanvas = document.createElement('canvas') - offScreenCanvas.width = canvas.width - offScreenCanvas.height = canvas.height - const offScreenCtx = offScreenCanvas.getContext('2d') + const offScreenCanvas = document.createElement('canvas'); + offScreenCanvas.width = canvas.width; + offScreenCanvas.height = canvas.height; + const offScreenCtx = offScreenCanvas.getContext('2d'); - let rasterizedCtx = ctx + let rasterizedCtx = ctx; // If we're not a 2d canvas we need to rasterise first into 2d - const rasterizeToCanvas = !(ctx instanceof CanvasRenderingContext2D) + const rasterizeToCanvas = !(ctx instanceof CanvasRenderingContext2D); if (rasterizeToCanvas) { // @ts-expect-error - Type 'CanvasRenderingContext2D | null' is not assignable to type 'CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext'. - rasterizedCtx = offScreenCtx + rasterizedCtx = offScreenCtx; // @ts-expect-error - 'offScreenCtx' is possibly 'null'. - offScreenCtx.drawImage(canvas, 0, 0) + offScreenCtx.drawImage(canvas, 0, 0); } // We *always* compute the random pixels on the complete pixel set, then pass back the subset later - let imageData = getImageDataProxy._native.apply(rasterizedCtx, [0, 0, canvas.width, canvas.height]) - imageData = modifyPixelData(imageData, sessionKey, domainKey, canvas.width) + let imageData = getImageDataProxy._native.apply(rasterizedCtx, [0, 0, canvas.width, canvas.height]); + imageData = modifyPixelData(imageData, sessionKey, domainKey, canvas.width); if (rasterizeToCanvas) { // @ts-expect-error - Type 'null' is not assignable to type 'CanvasRenderingContext2D'. - clearCanvas(offScreenCtx) + clearCanvas(offScreenCtx); } // @ts-expect-error - 'offScreenCtx' is possibly 'null'. - offScreenCtx.putImageData(imageData, 0, 0) + offScreenCtx.putImageData(imageData, 0, 0); - return { offScreenCanvas, offScreenCtx } + return { offScreenCanvas, offScreenCtx }; } /** @@ -50,13 +50,13 @@ export function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageD * * @param {CanvasRenderingContext2D} canvasContext */ -function clearCanvas (canvasContext) { +function clearCanvas(canvasContext) { // Save state and clean the pixels from the canvas - canvasContext.save() - canvasContext.globalCompositeOperation = 'destination-out' - canvasContext.fillStyle = 'rgb(255,255,255)' - canvasContext.fillRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height) - canvasContext.restore() + canvasContext.save(); + canvasContext.globalCompositeOperation = 'destination-out'; + canvasContext.fillStyle = 'rgb(255,255,255)'; + canvasContext.fillRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height); + canvasContext.restore(); } /** @@ -65,30 +65,30 @@ function clearCanvas (canvasContext) { * @param {string} domainKey * @param {number} width */ -export function modifyPixelData (imageData, domainKey, sessionKey, width) { - const d = imageData.data - const length = d.length / 4 - let checkSum = 0 - const mappingArray = [] +export function modifyPixelData(imageData, domainKey, sessionKey, width) { + const d = imageData.data; + const length = d.length / 4; + let checkSum = 0; + const mappingArray = []; for (let i = 0; i < length; i += 4) { if (!shouldIgnorePixel(d, i) && !adjacentSame(d, i, width)) { - mappingArray.push(i) - checkSum += d[i] + d[i + 1] + d[i + 2] + d[i + 3] + mappingArray.push(i); + checkSum += d[i] + d[i + 1] + d[i + 2] + d[i + 3]; } } - const windowHash = getDataKeySync(sessionKey, domainKey, checkSum) - const rng = new Seedrandom(windowHash) + const windowHash = getDataKeySync(sessionKey, domainKey, checkSum); + const rng = new Seedrandom(windowHash); for (let i = 0; i < mappingArray.length; i++) { - const rand = rng() - const byte = Math.floor(rand * 10) - const channel = byte % 3 - const pixelCanvasIndex = mappingArray[i] + channel + const rand = rng(); + const byte = Math.floor(rand * 10); + const channel = byte % 3; + const pixelCanvasIndex = mappingArray[i] + channel; - d[pixelCanvasIndex] = d[pixelCanvasIndex] ^ (byte & 0x1) + d[pixelCanvasIndex] = d[pixelCanvasIndex] ^ (byte & 0x1); } - return imageData + return imageData; } /** @@ -98,54 +98,54 @@ export function modifyPixelData (imageData, domainKey, sessionKey, width) { * @param {number} index * @param {number} width */ -function adjacentSame (imageData, index, width) { - const widthPixel = width * 4 - const x = index % widthPixel - const maxLength = imageData.length +function adjacentSame(imageData, index, width) { + const widthPixel = width * 4; + const x = index % widthPixel; + const maxLength = imageData.length; // Pixels not on the right border of the canvas if (x < widthPixel) { - const right = index + 4 + const right = index + 4; if (!pixelsSame(imageData, index, right)) { - return false + return false; } - const diagonalRightUp = right - widthPixel + const diagonalRightUp = right - widthPixel; if (diagonalRightUp > 0 && !pixelsSame(imageData, index, diagonalRightUp)) { - return false + return false; } - const diagonalRightDown = right + widthPixel + const diagonalRightDown = right + widthPixel; if (diagonalRightDown < maxLength && !pixelsSame(imageData, index, diagonalRightDown)) { - return false + return false; } } // Pixels not on the left border of the canvas if (x > 0) { - const left = index - 4 + const left = index - 4; if (!pixelsSame(imageData, index, left)) { - return false + return false; } - const diagonalLeftUp = left - widthPixel + const diagonalLeftUp = left - widthPixel; if (diagonalLeftUp > 0 && !pixelsSame(imageData, index, diagonalLeftUp)) { - return false + return false; } - const diagonalLeftDown = left + widthPixel + const diagonalLeftDown = left + widthPixel; if (diagonalLeftDown < maxLength && !pixelsSame(imageData, index, diagonalLeftDown)) { - return false + return false; } } - const up = index - widthPixel + const up = index - widthPixel; if (up > 0 && !pixelsSame(imageData, index, up)) { - return false + return false; } - const down = index + widthPixel + const down = index + widthPixel; if (down < maxLength && !pixelsSame(imageData, index, down)) { - return false + return false; } - return true + return true; } /** @@ -154,11 +154,13 @@ function adjacentSame (imageData, index, width) { * @param {number} index * @param {number} index2 */ -function pixelsSame (imageData, index, index2) { - return imageData[index] === imageData[index2] && - imageData[index + 1] === imageData[index2 + 1] && - imageData[index + 2] === imageData[index2 + 2] && - imageData[index + 3] === imageData[index2 + 3] +function pixelsSame(imageData, index, index2) { + return ( + imageData[index] === imageData[index2] && + imageData[index + 1] === imageData[index2 + 1] && + imageData[index + 2] === imageData[index2 + 2] && + imageData[index + 3] === imageData[index2 + 3] + ); } /** @@ -167,10 +169,10 @@ function pixelsSame (imageData, index, index2) { * @param {number} index * @returns {boolean} */ -function shouldIgnorePixel (imageData, index) { +function shouldIgnorePixel(imageData, index) { // Transparent pixels if (imageData[index + 3] === 0) { - return true + return true; } - return false + return false; } diff --git a/injected/src/captured-globals.js b/injected/src/captured-globals.js index e64ec34a5..047efd8fe 100644 --- a/injected/src/captured-globals.js +++ b/injected/src/captured-globals.js @@ -1,15 +1,15 @@ /* eslint-disable no-redeclare */ -export const Set = globalThis.Set -export const Reflect = globalThis.Reflect -export const customElementsGet = globalThis.customElements?.get.bind(globalThis.customElements) -export const customElementsDefine = globalThis.customElements?.define.bind(globalThis.customElements) -export const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor -export const getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors -export const objectKeys = Object.keys -export const objectEntries = Object.entries -export const objectDefineProperty = Object.defineProperty -export const URL = globalThis.URL -export const Proxy = globalThis.Proxy -export const functionToString = Function.prototype.toString -export const TypeError = globalThis.TypeError -export const Symbol = globalThis.Symbol +export const Set = globalThis.Set; +export const Reflect = globalThis.Reflect; +export const customElementsGet = globalThis.customElements?.get.bind(globalThis.customElements); +export const customElementsDefine = globalThis.customElements?.define.bind(globalThis.customElements); +export const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +export const getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors; +export const objectKeys = Object.keys; +export const objectEntries = Object.entries; +export const objectDefineProperty = Object.defineProperty; +export const URL = globalThis.URL; +export const Proxy = globalThis.Proxy; +export const functionToString = Function.prototype.toString; +export const TypeError = globalThis.TypeError; +export const Symbol = globalThis.Symbol; diff --git a/injected/src/content-feature.js b/injected/src/content-feature.js index bd570ffbe..4ffa13141 100644 --- a/injected/src/content-feature.js +++ b/injected/src/content-feature.js @@ -1,11 +1,11 @@ -import { camelcase, matchHostname, processAttr, computeEnabledFeatures, parseFeatureSettings } from './utils.js' -import { immutableJSONPatch } from 'immutable-json-patch' -import { PerformanceMonitor } from './performance.js' -import { defineProperty, shimInterface, shimProperty, wrapMethod, wrapProperty, wrapToString } from './wrapper-utils.js' +import { camelcase, matchHostname, processAttr, computeEnabledFeatures, parseFeatureSettings } from './utils.js'; +import { immutableJSONPatch } from 'immutable-json-patch'; +import { PerformanceMonitor } from './performance.js'; +import { defineProperty, shimInterface, shimProperty, wrapMethod, wrapProperty, wrapToString } from './wrapper-utils.js'; // eslint-disable-next-line no-redeclare -import { Proxy, Reflect } from './captured-globals.js' -import { Messaging, MessagingContext } from '../../messaging/index.js' -import { extensionConstructMessagingConfig } from './sendmessage-transport.js' +import { Proxy, Reflect } from './captured-globals.js'; +import { Messaging, MessagingContext } from '../../messaging/index.js'; +import { extensionConstructMessagingConfig } from './sendmessage-transport.js'; /** * @typedef {object} AssetConfig @@ -23,96 +23,94 @@ import { extensionConstructMessagingConfig } from './sendmessage-transport.js' export default class ContentFeature { /** @type {import('./utils.js').RemoteConfig | undefined} */ - #bundledConfig + #bundledConfig; /** @type {object | undefined} */ - #trackerLookup + #trackerLookup; /** @type {boolean | undefined} */ - #documentOriginIsTracker + #documentOriginIsTracker; /** @type {Record | undefined} */ // eslint-disable-next-line no-unused-private-class-members - #bundledfeatureSettings + #bundledfeatureSettings; /** @type {import('../../messaging').Messaging} */ // eslint-disable-next-line no-unused-private-class-members - #messaging + #messaging; /** @type {boolean} */ - #isDebugFlagSet = false + #isDebugFlagSet = false; /** @type {{ debug?: boolean, desktopModeEnabled?: boolean, forcedZoomEnabled?: boolean, featureSettings?: Record, assets?: AssetConfig | undefined, site: Site, messagingConfig?: import('@duckduckgo/messaging').MessagingConfig } | null} */ - #args + #args; - constructor (featureName) { - this.name = featureName - this.#args = null - this.monitor = new PerformanceMonitor() + constructor(featureName) { + this.name = featureName; + this.#args = null; + this.monitor = new PerformanceMonitor(); } - get isDebug () { - return this.#args?.debug || false + get isDebug() { + return this.#args?.debug || false; } - get desktopModeEnabled () { - return this.#args?.desktopModeEnabled || false + get desktopModeEnabled() { + return this.#args?.desktopModeEnabled || false; } - get forcedZoomEnabled () { - return this.#args?.forcedZoomEnabled || false + get forcedZoomEnabled() { + return this.#args?.forcedZoomEnabled || false; } /** * @param {import('./utils').Platform} platform */ - set platform (platform) { - this._platform = platform + set platform(platform) { + this._platform = platform; } - get platform () { + get platform() { // @ts-expect-error - Type 'Platform | undefined' is not assignable to type 'Platform' - return this._platform + return this._platform; } /** * @type {AssetConfig | undefined} */ - get assetConfig () { - return this.#args?.assets + get assetConfig() { + return this.#args?.assets; } /** * @returns {boolean} */ - get documentOriginIsTracker () { - return !!this.#documentOriginIsTracker + get documentOriginIsTracker() { + return !!this.#documentOriginIsTracker; } /** * @returns {object} **/ - get trackerLookup () { - return this.#trackerLookup || {} + get trackerLookup() { + return this.#trackerLookup || {}; } /** * @returns {import('./utils.js').RemoteConfig | undefined} **/ - get bundledConfig () { - return this.#bundledConfig + get bundledConfig() { + return this.#bundledConfig; } /** * @deprecated as we should make this internal to the class and not used externally * @return {MessagingContext} */ - _createMessagingContext () { - const injectName = import.meta.injectName - const contextName = injectName === 'apple-isolated' - ? 'contentScopeScriptsIsolated' - : 'contentScopeScripts' + _createMessagingContext() { + const injectName = import.meta.injectName; + const contextName = injectName === 'apple-isolated' ? 'contentScopeScriptsIsolated' : 'contentScopeScripts'; return new MessagingContext({ context: contextName, env: this.isDebug ? 'development' : 'production', - featureName: this.name - }) + featureName: this.name, + }); } /** @@ -120,16 +118,16 @@ export default class ContentFeature { * * @return {import('@duckduckgo/messaging').Messaging} */ - get messaging () { - if (this._messaging) return this._messaging - const messagingContext = this._createMessagingContext() - let messagingConfig = this.#args?.messagingConfig + get messaging() { + if (this._messaging) return this._messaging; + const messagingContext = this._createMessagingContext(); + let messagingConfig = this.#args?.messagingConfig; if (!messagingConfig) { - if (this.platform?.name !== 'extension') throw new Error('Only extension messaging supported, all others should be passed in') - messagingConfig = extensionConstructMessagingConfig() + if (this.platform?.name !== 'extension') throw new Error('Only extension messaging supported, all others should be passed in'); + messagingConfig = extensionConstructMessagingConfig(); } - this._messaging = new Messaging(messagingContext, messagingConfig) - return this._messaging + this._messaging = new Messaging(messagingContext, messagingConfig); + return this._messaging; } /** @@ -141,9 +139,9 @@ export default class ContentFeature { * @param {any} defaultValue - The default value to use if the config setting is not set * @returns The value of the config setting or the default value */ - getFeatureAttr (attrName, defaultValue) { - const configSetting = this.getFeatureSetting(attrName) - return processAttr(configSetting, defaultValue) + getFeatureAttr(attrName, defaultValue) { + const configSetting = this.getFeatureSetting(attrName); + return processAttr(configSetting, defaultValue); } /** @@ -152,25 +150,25 @@ export default class ContentFeature { * @param {string} [featureName] * @returns {any} */ - getFeatureSetting (featureKeyName, featureName) { - let result = this._getFeatureSettings(featureName) + getFeatureSetting(featureKeyName, featureName) { + let result = this._getFeatureSettings(featureName); if (featureKeyName === 'domains') { - throw new Error('domains is a reserved feature setting key name') + throw new Error('domains is a reserved feature setting key name'); } const domainMatch = [...this.matchDomainFeatureSetting('domains')].sort((a, b) => { - return a.domain.length - b.domain.length - }) + return a.domain.length - b.domain.length; + }); for (const match of domainMatch) { if (match.patchSettings === undefined) { - continue + continue; } try { - result = immutableJSONPatch(result, match.patchSettings) + result = immutableJSONPatch(result, match.patchSettings); } catch (e) { - console.error('Error applying patch settings', e) + console.error('Error applying patch settings', e); } } - return result?.[featureKeyName] + return result?.[featureKeyName]; } /** @@ -178,9 +176,9 @@ export default class ContentFeature { * @param {string} [featureName] - The name of the feature to get the settings for; defaults to the name of the feature * @returns {any} */ - _getFeatureSettings (featureName) { - const camelFeatureName = featureName || camelcase(this.name) - return this.#args?.featureSettings?.[camelFeatureName] + _getFeatureSettings(featureName) { + const camelFeatureName = featureName || camelcase(this.name); + return this.#args?.featureSettings?.[camelFeatureName]; } /** @@ -190,12 +188,12 @@ export default class ContentFeature { * @param {string} [featureName] * @returns {boolean} */ - getFeatureSettingEnabled (featureKeyName, featureName) { - const result = this.getFeatureSetting(featureKeyName, featureName) + getFeatureSettingEnabled(featureKeyName, featureName) { + const result = this.getFeatureSetting(featureKeyName, featureName); if (typeof result === 'object') { - return result.state === 'enabled' + return result.state === 'enabled'; } - return result === 'enabled' + return result === 'enabled'; } /** @@ -203,36 +201,32 @@ export default class ContentFeature { * @param {string} featureKeyName * @return {any[]} */ - matchDomainFeatureSetting (featureKeyName) { - const domain = this.#args?.site.domain - if (!domain) return [] - const domains = this._getFeatureSettings()?.[featureKeyName] || [] + matchDomainFeatureSetting(featureKeyName) { + const domain = this.#args?.site.domain; + if (!domain) return []; + const domains = this._getFeatureSettings()?.[featureKeyName] || []; return domains.filter((rule) => { if (Array.isArray(rule.domain)) { return rule.domain.some((domainRule) => { - return matchHostname(domain, domainRule) - }) + return matchHostname(domain, domainRule); + }); } - return matchHostname(domain, rule.domain) - }) + return matchHostname(domain, rule.domain); + }); } - - init (args) { - } + init(args) {} - callInit (args) { - const mark = this.monitor.mark(this.name + 'CallInit') - this.#args = args - this.platform = args.platform - this.init(args) - mark.end() - this.measure() + callInit(args) { + const mark = this.monitor.mark(this.name + 'CallInit'); + this.#args = args; + this.platform = args.platform; + this.init(args); + mark.end(); + this.measure(); } - - load (args) { - } + load(args) {} /** * This is a wrapper around `this.messaging.notify` that applies the @@ -242,9 +236,9 @@ export default class ContentFeature { * * @type {import("@duckduckgo/messaging").Messaging['notify']} */ - notify (...args) { - const [name, params] = args - this.messaging.notify(name, params) + notify(...args) { + const [name, params] = args; + this.messaging.notify(name, params); } /** @@ -255,9 +249,9 @@ export default class ContentFeature { * * @type {import("@duckduckgo/messaging").Messaging['request']} */ - request (...args) { - const [name, params] = args - return this.messaging.request(name, params) + request(...args) { + const [name, params] = args; + return this.messaging.request(name, params); } /** @@ -268,50 +262,48 @@ export default class ContentFeature { * * @type {import("@duckduckgo/messaging").Messaging['subscribe']} */ - subscribe (...args) { - const [name, cb] = args - return this.messaging.subscribe(name, cb) + subscribe(...args) { + const [name, cb] = args; + return this.messaging.subscribe(name, cb); } /** * @param {import('./content-scope-features.js').LoadArgs} args */ - callLoad (args) { - const mark = this.monitor.mark(this.name + 'CallLoad') - this.#args = args - this.platform = args.platform - this.#bundledConfig = args.bundledConfig + callLoad(args) { + const mark = this.monitor.mark(this.name + 'CallLoad'); + this.#args = args; + this.platform = args.platform; + this.#bundledConfig = args.bundledConfig; // If we have a bundled config, treat it as a regular config // This will be overriden by the remote config if it is available if (this.#bundledConfig && this.#args) { - const enabledFeatures = computeEnabledFeatures(args.bundledConfig, args.site.domain, this.platform.version) - this.#args.featureSettings = parseFeatureSettings(args.bundledConfig, enabledFeatures) + const enabledFeatures = computeEnabledFeatures(args.bundledConfig, args.site.domain, this.platform.version); + this.#args.featureSettings = parseFeatureSettings(args.bundledConfig, enabledFeatures); } - this.#trackerLookup = args.trackerLookup - this.#documentOriginIsTracker = args.documentOriginIsTracker - this.load(args) - mark.end() + this.#trackerLookup = args.trackerLookup; + this.#documentOriginIsTracker = args.documentOriginIsTracker; + this.load(args); + mark.end(); } - measure () { + measure() { if (this.#args?.debug) { - this.monitor.measureAll() + this.monitor.measureAll(); } } - - update () { - } + update() {} /** * Register a flag that will be added to page breakage reports */ - addDebugFlag () { - if (this.#isDebugFlagSet) return - this.#isDebugFlagSet = true + addDebugFlag() { + if (this.#isDebugFlagSet) return; + this.#isDebugFlagSet = true; this.messaging?.notify('addDebugFlag', { - flag: this.name - }) + flag: this.name, + }); } /** @@ -321,24 +313,24 @@ export default class ContentFeature { * @param {string} propertyName * @param {import('./wrapper-utils').StrictPropertyDescriptor} descriptor - requires all descriptor options to be defined because we can't validate correctness based on TS types */ - defineProperty (object, propertyName, descriptor) { + defineProperty(object, propertyName, descriptor) { // make sure to send a debug flag when the property is used // NOTE: properties passing data in `value` would not be caught by this ['value', 'get', 'set'].forEach((k) => { - const descriptorProp = descriptor[k] + const descriptorProp = descriptor[k]; if (typeof descriptorProp === 'function') { - const addDebugFlag = this.addDebugFlag.bind(this) + const addDebugFlag = this.addDebugFlag.bind(this); const wrapper = new Proxy(descriptorProp, { - apply (target, thisArg, argumentsList) { - addDebugFlag() - return Reflect.apply(descriptorProp, thisArg, argumentsList) - } - }) - descriptor[k] = wrapToString(wrapper, descriptorProp) + apply(target, thisArg, argumentsList) { + addDebugFlag(); + return Reflect.apply(descriptorProp, thisArg, argumentsList); + }, + }); + descriptor[k] = wrapToString(wrapper, descriptorProp); } - }) + }); - return defineProperty(object, propertyName, descriptor) + return defineProperty(object, propertyName, descriptor); } /** @@ -348,8 +340,8 @@ export default class ContentFeature { * @param {Partial} descriptor * @returns {PropertyDescriptor|undefined} original property descriptor, or undefined if it's not found */ - wrapProperty (object, propertyName, descriptor) { - return wrapProperty(object, propertyName, descriptor, this.defineProperty.bind(this)) + wrapProperty(object, propertyName, descriptor) { + return wrapProperty(object, propertyName, descriptor, this.defineProperty.bind(this)); } /** @@ -359,8 +351,8 @@ export default class ContentFeature { * @param {(originalFn, ...args) => any } wrapperFn - wrapper function receives the original function as the first argument * @returns {PropertyDescriptor|undefined} original property descriptor, or undefined if it's not found */ - wrapMethod (object, propertyName, wrapperFn) { - return wrapMethod(object, propertyName, wrapperFn, this.defineProperty.bind(this)) + wrapMethod(object, propertyName, wrapperFn) { + return wrapMethod(object, propertyName, wrapperFn, this.defineProperty.bind(this)); } /** @@ -369,12 +361,8 @@ export default class ContentFeature { * @param {typeof globalThis[StandardInterfaceName]} ImplClass - the class to use as the shim implementation * @param {import('./wrapper-utils').DefineInterfaceOptions} options */ - shimInterface ( - interfaceName, - ImplClass, - options - ) { - return shimInterface(interfaceName, ImplClass, options, this.defineProperty.bind(this)) + shimInterface(interfaceName, ImplClass, options) { + return shimInterface(interfaceName, ImplClass, options, this.defineProperty.bind(this)); } /** @@ -388,7 +376,7 @@ export default class ContentFeature { * @param {Base[K]} implInstance - instance to use as the shim (e.g. new MyMediaSession()) * @param {boolean} [readOnly] - whether the property should be read-only (default: false) */ - shimProperty (instanceHost, instanceProp, implInstance, readOnly = false) { - return shimProperty(instanceHost, instanceProp, implInstance, readOnly, this.defineProperty.bind(this)) + shimProperty(instanceHost, instanceProp, implInstance, readOnly = false) { + return shimProperty(instanceHost, instanceProp, implInstance, readOnly, this.defineProperty.bind(this)); } } diff --git a/injected/src/content-scope-features.js b/injected/src/content-scope-features.js index 9300ae760..c7d56e850 100644 --- a/injected/src/content-scope-features.js +++ b/injected/src/content-scope-features.js @@ -1,13 +1,13 @@ -import { initStringExemptionLists, isFeatureBroken, registerMessageSecret } from './utils' -import { platformSupport } from './features' -import { PerformanceMonitor } from './performance' -import platformFeatures from 'ddg:platformFeatures' +import { initStringExemptionLists, isFeatureBroken, registerMessageSecret } from './utils'; +import { platformSupport } from './features'; +import { PerformanceMonitor } from './performance'; +import platformFeatures from 'ddg:platformFeatures'; -let initArgs = null -const updates = [] -const features = [] -const alwaysInitFeatures = new Set(['cookie']) -const performanceMonitor = new PerformanceMonitor() +let initArgs = null; +const updates = []; +const features = []; +const alwaysInitFeatures = new Set(['cookie']); +const performanceMonitor = new PerformanceMonitor(); // It's important to avoid enabling the features for non-HTML documents (such as // XML documents that aren't XHTML). Note that it's necessary to check the @@ -15,12 +15,8 @@ const performanceMonitor = new PerformanceMonitor() // checks by altering document.__proto__. In the future, it might be worth // running the checks even earlier (and in the "isolated world" for the Chrome // extension), to further reduce that risk. -const isHTMLDocument = ( - document instanceof HTMLDocument || ( - document instanceof XMLDocument && - document.createElement('div') instanceof HTMLDivElement - ) -) +const isHTMLDocument = + document instanceof HTMLDocument || (document instanceof XMLDocument && document.createElement('div') instanceof HTMLDivElement); /** * @typedef {object} LoadArgs @@ -36,70 +32,68 @@ const isHTMLDocument = ( /** * @param {LoadArgs} args */ -export function load (args) { - const mark = performanceMonitor.mark('load') +export function load(args) { + const mark = performanceMonitor.mark('load'); if (!isHTMLDocument) { - return + return; } - const featureNames = typeof import.meta.injectName === 'string' - ? platformSupport[import.meta.injectName] - : [] + const featureNames = typeof import.meta.injectName === 'string' ? platformSupport[import.meta.injectName] : []; for (const featureName of featureNames) { - const ContentFeature = platformFeatures['ddg_feature_' + featureName] - const featureInstance = new ContentFeature(featureName) - featureInstance.callLoad(args) - features.push({ featureName, featureInstance }) + const ContentFeature = platformFeatures['ddg_feature_' + featureName]; + const featureInstance = new ContentFeature(featureName); + featureInstance.callLoad(args); + features.push({ featureName, featureInstance }); } - mark.end() + mark.end(); } -export async function init (args) { - const mark = performanceMonitor.mark('init') - initArgs = args +export async function init(args) { + const mark = performanceMonitor.mark('init'); + initArgs = args; if (!isHTMLDocument) { - return + return; } - registerMessageSecret(args.messageSecret) - initStringExemptionLists(args) - const resolvedFeatures = await Promise.all(features) + registerMessageSecret(args.messageSecret); + initStringExemptionLists(args); + const resolvedFeatures = await Promise.all(features); resolvedFeatures.forEach(({ featureInstance, featureName }) => { if (!isFeatureBroken(args, featureName) || alwaysInitExtensionFeatures(args, featureName)) { - featureInstance.callInit(args) + featureInstance.callInit(args); } - }) + }); // Fire off updates that came in faster than the init while (updates.length) { - const update = updates.pop() - await updateFeaturesInner(update) + const update = updates.pop(); + await updateFeaturesInner(update); } - mark.end() + mark.end(); if (args.debug) { - performanceMonitor.measureAll() + performanceMonitor.measureAll(); } } -export function update (args) { +export function update(args) { if (!isHTMLDocument) { - return + return; } if (initArgs === null) { - updates.push(args) - return + updates.push(args); + return; } - updateFeaturesInner(args) + updateFeaturesInner(args); } -function alwaysInitExtensionFeatures (args, featureName) { - return args.platform.name === 'extension' && alwaysInitFeatures.has(featureName) +function alwaysInitExtensionFeatures(args, featureName) { + return args.platform.name === 'extension' && alwaysInitFeatures.has(featureName); } -async function updateFeaturesInner (args) { - const resolvedFeatures = await Promise.all(features) +async function updateFeaturesInner(args) { + const resolvedFeatures = await Promise.all(features); resolvedFeatures.forEach(({ featureInstance, featureName }) => { if (!isFeatureBroken(initArgs, featureName) && featureInstance.update) { - featureInstance.update(args) + featureInstance.update(args); } - }) + }); } diff --git a/injected/src/cookie.js b/injected/src/cookie.js index fbf5e4a74..91fca64a2 100644 --- a/injected/src/cookie.js +++ b/injected/src/cookie.js @@ -1,55 +1,55 @@ export class Cookie { - constructor (cookieString) { - this.parts = cookieString.split(';') - this.parse() + constructor(cookieString) { + this.parts = cookieString.split(';'); + this.parse(); } - parse () { - const EXTRACT_ATTRIBUTES = new Set(['max-age', 'expires', 'domain']) - this.attrIdx = {} + parse() { + const EXTRACT_ATTRIBUTES = new Set(['max-age', 'expires', 'domain']); + this.attrIdx = {}; this.parts.forEach((part, index) => { - const kv = part.split('=', 1) - const attribute = kv[0].trim() - const value = part.slice(kv[0].length + 1) + const kv = part.split('=', 1); + const attribute = kv[0].trim(); + const value = part.slice(kv[0].length + 1); if (index === 0) { - this.name = attribute - this.value = value + this.name = attribute; + this.value = value; } else if (EXTRACT_ATTRIBUTES.has(attribute.toLowerCase())) { - this[attribute.toLowerCase()] = value + this[attribute.toLowerCase()] = value; // @ts-expect-error - Object is possibly 'undefined'. - this.attrIdx[attribute.toLowerCase()] = index + this.attrIdx[attribute.toLowerCase()] = index; } - }) + }); } - getExpiry () { + getExpiry() { // @ts-expect-error expires is not defined in the type definition if (!this.maxAge && !this.expires) { - return NaN + return NaN; } const expiry = this.maxAge ? parseInt(this.maxAge) - // @ts-expect-error expires is not defined in the type definition - : (new Date(this.expires) - new Date()) / 1000 - return expiry + : // @ts-expect-error expires is not defined in the type definition + (new Date(this.expires) - new Date()) / 1000; + return expiry; } - get maxAge () { - return this['max-age'] + get maxAge() { + return this['max-age']; } - set maxAge (value) { + set maxAge(value) { // @ts-expect-error - Object is possibly 'undefined'. if (this.attrIdx['max-age'] > 0) { // @ts-expect-error - Object is possibly 'undefined'. - this.parts.splice(this.attrIdx['max-age'], 1, `max-age=${value}`) + this.parts.splice(this.attrIdx['max-age'], 1, `max-age=${value}`); } else { - this.parts.push(`max-age=${value}`) + this.parts.push(`max-age=${value}`); } - this.parse() + this.parse(); } - toString () { - return this.parts.join(';') + toString() { + return this.parts.join(';'); } } diff --git a/injected/src/crypto.js b/injected/src/crypto.js index add65aaa8..ece87025a 100644 --- a/injected/src/crypto.js +++ b/injected/src/crypto.js @@ -1,7 +1,7 @@ -import { sjcl } from '../lib/sjcl.js' +import { sjcl } from '../lib/sjcl.js'; -export function getDataKeySync (sessionKey, domainKey, inputData) { +export function getDataKeySync(sessionKey, domainKey, inputData) { // eslint-disable-next-line new-cap - const hmac = new sjcl.misc.hmac(sjcl.codec.utf8String.toBits(sessionKey + domainKey), sjcl.hash.sha256) - return sjcl.codec.hex.fromBits(hmac.encrypt(inputData)) + const hmac = new sjcl.misc.hmac(sjcl.codec.utf8String.toBits(sessionKey + domainKey), sjcl.hash.sha256); + return sjcl.codec.hex.fromBits(hmac.encrypt(inputData)); } diff --git a/injected/src/dom-utils.js b/injected/src/dom-utils.js index 0784fa645..65ca1f207 100644 --- a/injected/src/dom-utils.js +++ b/injected/src/dom-utils.js @@ -2,9 +2,9 @@ * The following code is originally from https://github.com/mozilla-extensions/secure-proxy/blob/db4d1b0e2bfe0abae416bf04241916f9e4768fd2/src/commons/template.js */ class Template { - constructor (strings, values) { - this.values = values - this.strings = strings + constructor(strings, values) { + this.values = values; + this.strings = strings; } /** @@ -14,68 +14,68 @@ class Template { * The string to escape. * @return {string} The escaped string. */ - escapeXML (str) { + escapeXML(str) { const replacements = { '&': '&', '"': '"', "'": ''', '<': '<', '>': '>', - '/': '/' - } - return String(str).replace(/[&"'<>/]/g, m => replacements[m]) + '/': '/', + }; + return String(str).replace(/[&"'<>/]/g, (m) => replacements[m]); } - potentiallyEscape (value) { + potentiallyEscape(value) { if (typeof value === 'object') { if (value instanceof Array) { - return value.map(val => this.potentiallyEscape(val)).join('') + return value.map((val) => this.potentiallyEscape(val)).join(''); } // If we are an escaped template let join call toString on it if (value instanceof Template) { - return value + return value; } - throw new Error('Unknown object to escape') + throw new Error('Unknown object to escape'); } - return this.escapeXML(value) + return this.escapeXML(value); } - toString () { - const result = [] + toString() { + const result = []; for (const [i, string] of this.strings.entries()) { - result.push(string) + result.push(string); if (i < this.values.length) { - result.push(this.potentiallyEscape(this.values[i])) + result.push(this.potentiallyEscape(this.values[i])); } } - return result.join('') + return result.join(''); } } -export function html (strings, ...values) { - return new Template(strings, values) +export function html(strings, ...values) { + return new Template(strings, values); } /** * @param {string} string * @return {Template} */ -export function trustedUnsafe (string) { - return html([string]) +export function trustedUnsafe(string) { + return html([string]); } /** * Use a policy if trustedTypes is available * @return {{createHTML: (s: string) => any}} */ -export function createPolicy () { +export function createPolicy() { if (globalThis.trustedTypes) { - return globalThis.trustedTypes?.createPolicy?.('ddg-default', { createHTML: (s) => s }) + return globalThis.trustedTypes?.createPolicy?.('ddg-default', { createHTML: (s) => s }); } return { - createHTML: (s) => s - } + createHTML: (s) => s, + }; } diff --git a/injected/src/features.js b/injected/src/features.js index 4643d75a4..f20e826b4 100644 --- a/injected/src/features.js +++ b/injected/src/features.js @@ -1,4 +1,4 @@ -export const baseFeatures = /** @type {const} */([ +export const baseFeatures = /** @type {const} */ ([ 'fingerprintingAudio', 'fingerprintingBattery', 'fingerprintingCanvas', @@ -10,10 +10,10 @@ export const baseFeatures = /** @type {const} */([ 'fingerprintingTemporaryStorage', 'navigatorInterface', 'elementHiding', - 'exceptionHandler' -]) + 'exceptionHandler', +]); -const otherFeatures = /** @type {const} */([ +const otherFeatures = /** @type {const} */ ([ 'clickToLoad', 'cookie', 'duckPlayer', @@ -23,57 +23,19 @@ const otherFeatures = /** @type {const} */([ 'brokerProtection', 'performanceMetrics', 'breakageReporting', - 'autofillPasswordImport' -]) + 'autofillPasswordImport', +]); /** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */ /** @type {Record} */ export const platformSupport = { - apple: [ - 'webCompat', - ...baseFeatures - ], - 'apple-isolated': [ - 'duckPlayer', - 'brokerProtection', - 'performanceMetrics', - 'clickToLoad' - ], - android: [ - ...baseFeatures, - 'webCompat', - 'clickToLoad', - 'breakageReporting', - 'duckPlayer' - ], - 'android-autofill-password-import': [ - 'autofillPasswordImport' - ], - windows: [ - 'cookie', - ...baseFeatures, - 'windowsPermissionUsage', - 'duckPlayer', - 'brokerProtection', - 'breakageReporting' - ], - firefox: [ - 'cookie', - ...baseFeatures, - 'clickToLoad' - ], - chrome: [ - 'cookie', - ...baseFeatures, - 'clickToLoad' - ], - 'chrome-mv3': [ - 'cookie', - ...baseFeatures, - 'clickToLoad' - ], - integration: [ - ...baseFeatures, - ...otherFeatures - ] -} + apple: ['webCompat', ...baseFeatures], + 'apple-isolated': ['duckPlayer', 'brokerProtection', 'performanceMetrics', 'clickToLoad'], + android: [...baseFeatures, 'webCompat', 'clickToLoad', 'breakageReporting', 'duckPlayer'], + 'android-autofill-password-import': ['autofillPasswordImport'], + windows: ['cookie', ...baseFeatures, 'windowsPermissionUsage', 'duckPlayer', 'brokerProtection', 'breakageReporting'], + firefox: ['cookie', ...baseFeatures, 'clickToLoad'], + chrome: ['cookie', ...baseFeatures, 'clickToLoad'], + 'chrome-mv3': ['cookie', ...baseFeatures, 'clickToLoad'], + integration: [...baseFeatures, ...otherFeatures], +}; diff --git a/injected/src/features/autofill-password-import.js b/injected/src/features/autofill-password-import.js index 2b846cc8f..0be34fa7f 100644 --- a/injected/src/features/autofill-password-import.js +++ b/injected/src/features/autofill-password-import.js @@ -1,8 +1,8 @@ -import ContentFeature from '../content-feature' -import { DDGProxy, DDGReflect, withExponentialBackoff } from '../utils' +import ContentFeature from '../content-feature'; +import { DDGProxy, DDGReflect, withExponentialBackoff } from '../utils'; -const ANIMATION_DURATION_MS = 1000 -const ANIMATION_ITERATIONS = Infinity +const ANIMATION_DURATION_MS = 1000; +const ANIMATION_ITERATIONS = Infinity; /** * This feature is responsible for animating some buttons passwords.google.com, @@ -12,38 +12,38 @@ const ANIMATION_ITERATIONS = Infinity * 3. Animate the element, or tap it if it should be autotapped. */ export default class AutofillPasswordImport extends ContentFeature { - #exportButtonSettings - #settingsButtonSettings - #signInButtonSettings + #exportButtonSettings; + #settingsButtonSettings; + #signInButtonSettings; /** * @returns {any} */ - get settingsButtonStyle () { + get settingsButtonStyle() { return { scale: 1, - backgroundColor: 'rgba(0, 39, 142, 0.5)' - } + backgroundColor: 'rgba(0, 39, 142, 0.5)', + }; } /** * @returns {any} */ - get exportButtonStyle () { + get exportButtonStyle() { return { scale: 1.01, - backgroundColor: 'rgba(0, 39, 142, 0.5)' - } + backgroundColor: 'rgba(0, 39, 142, 0.5)', + }; } /** * @returns {any} */ - get signInButtonStyle () { + get signInButtonStyle() { return { scale: 1.5, - backgroundColor: 'rgba(0, 39, 142, 0.5)' - } + backgroundColor: 'rgba(0, 39, 142, 0.5)', + }; } /** @@ -51,36 +51,36 @@ export default class AutofillPasswordImport extends ContentFeature { * @param {string} path * @returns {Promise<{element: HTMLElement|Element, style: any, shouldTap: boolean}|null>} */ - async getElementAndStyleFromPath (path) { + async getElementAndStyleFromPath(path) { if (path === '/') { - const element = await this.findSettingsElement() + const element = await this.findSettingsElement(); return element != null ? { - style: this.settingsButtonStyle, - element, - shouldTap: this.#settingsButtonSettings?.shouldAutotap ?? false - } - : null + style: this.settingsButtonStyle, + element, + shouldTap: this.#settingsButtonSettings?.shouldAutotap ?? false, + } + : null; } else if (path === '/options') { - const element = await this.findExportElement() + const element = await this.findExportElement(); return element != null ? { - style: this.exportButtonStyle, - element, - shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false - } - : null + style: this.exportButtonStyle, + element, + shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false, + } + : null; } else if (path === '/intro') { - const element = await this.findSignInButton() + const element = await this.findSignInButton(); return element != null ? { - style: this.signInButtonStyle, - element, - shouldTap: this.#signInButtonSettings?.shouldAutotap ?? false - } - : null + style: this.signInButtonStyle, + element, + shouldTap: this.#signInButtonSettings?.shouldAutotap ?? false, + } + : null; } else { - return null + return null; } } @@ -89,30 +89,30 @@ export default class AutofillPasswordImport extends ContentFeature { * @param {HTMLElement|Element} element * @param {any} style */ - animateElement (element, style) { + animateElement(element, style) { element.scrollIntoView({ behavior: 'smooth', block: 'center', - inline: 'center' - }) // Scroll into view + inline: 'center', + }); // Scroll into view const keyframes = [ { backgroundColor: 'rgba(0, 0, 255, 0)', offset: 0, borderRadius: '2px' }, // Start: transparent { backgroundColor: style.backgroundColor, offset: 0.5, borderRadius: '2px', transform: `scale(${style.scale})` }, // Midpoint: blue with 50% opacity - { backgroundColor: 'rgba(0, 0, 255, 0)', borderRadius: '2px', offset: 1 } // End: transparent - ] + { backgroundColor: 'rgba(0, 0, 255, 0)', borderRadius: '2px', offset: 1 }, // End: transparent + ]; // Define the animation options const options = { duration: ANIMATION_DURATION_MS, - iterations: ANIMATION_ITERATIONS - } + iterations: ANIMATION_ITERATIONS, + }; // Apply the animation to the element - element.animate(keyframes, options) + element.animate(keyframes, options); } - autotapElement (element) { - element.click() + autotapElement(element) { + element.click(); } /** @@ -121,56 +121,52 @@ export default class AutofillPasswordImport extends ContentFeature { * If that fails, we look for the button based on it's label. * @returns {Promise} */ - async findExportElement () { + async findExportElement() { const findInContainer = () => { - const exportButtonContainer = document.querySelector(this.exportButtonContainerSelector) - return exportButtonContainer && exportButtonContainer.querySelectorAll('button')[1] - } + const exportButtonContainer = document.querySelector(this.exportButtonContainerSelector); + return exportButtonContainer && exportButtonContainer.querySelectorAll('button')[1]; + }; const findWithLabel = () => { - return document.querySelector(this.exportButtonLabelTextSelector) - } + return document.querySelector(this.exportButtonLabelTextSelector); + }; - return await withExponentialBackoff(() => findInContainer() ?? findWithLabel()) + return await withExponentialBackoff(() => findInContainer() ?? findWithLabel()); } /** * @returns {Promise} */ - async findSettingsElement () { + async findSettingsElement() { const fn = () => { - const settingsButton = document.querySelector(this.settingsButtonSelector) - return settingsButton - } - return await withExponentialBackoff(fn) + const settingsButton = document.querySelector(this.settingsButtonSelector); + return settingsButton; + }; + return await withExponentialBackoff(fn); } /** * @returns {Promise} */ - async findSignInButton () { - return await withExponentialBackoff(() => document.querySelector(this.signinButtonSelector)) + async findSignInButton() { + return await withExponentialBackoff(() => document.querySelector(this.signinButtonSelector)); } /** * Checks if the path is supported and animates/taps the element if it is. * @param {string} path */ - async handleElementForPath (path) { - const supportedPaths = [ - this.#exportButtonSettings?.path, - this.#settingsButtonSettings?.path, - this.#signInButtonSettings?.path - ] + async handleElementForPath(path) { + const supportedPaths = [this.#exportButtonSettings?.path, this.#settingsButtonSettings?.path, this.#signInButtonSettings?.path]; if (supportedPaths.indexOf(path)) { try { - const { element, style, shouldTap } = await this.getElementAndStyleFromPath(path) ?? {} + const { element, style, shouldTap } = (await this.getElementAndStyleFromPath(path)) ?? {}; if (element != null) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - shouldTap ? this.autotapElement(element) : this.animateElement(element, style) + shouldTap ? this.autotapElement(element) : this.animateElement(element, style); } } catch { - console.error('password-import: handleElementForPath failed for path:', path) + console.error('password-import: handleElementForPath failed for path:', path); } } } @@ -178,76 +174,70 @@ export default class AutofillPasswordImport extends ContentFeature { /** * @returns {string} */ - get exportButtonContainerSelector () { - return this.#exportButtonSettings?.selectors?.join(',') + get exportButtonContainerSelector() { + return this.#exportButtonSettings?.selectors?.join(','); } /** * @returns {string} */ - get exportButtonLabelTextSelector () { - return this.#exportButtonSettings?.labelTexts - .map(text => `button[aria-label="${text}"]`) - .join(',') + get exportButtonLabelTextSelector() { + return this.#exportButtonSettings?.labelTexts.map((text) => `button[aria-label="${text}"]`).join(','); } /** * @returns {string} */ - get signinLabelTextSelector () { - return this.#signInButtonSettings?.labelTexts - .map(text => `a[aria-label="${text}"]:not([target="_top"])`) - .join(',') + get signinLabelTextSelector() { + return this.#signInButtonSettings?.labelTexts.map((text) => `a[aria-label="${text}"]:not([target="_top"])`).join(','); } /** * @returns {string} */ - get signinButtonSelector () { - return `${this.#signInButtonSettings?.selectors?.join(',')}, ${this.signinLabelTextSelector}` + get signinButtonSelector() { + return `${this.#signInButtonSettings?.selectors?.join(',')}, ${this.signinLabelTextSelector}`; } /** * @returns {string} */ - get settingsLabelTextSelector () { - return this.#settingsButtonSettings?.labelTexts - .map(text => `a[aria-label="${text}"]`) - .join(',') + get settingsLabelTextSelector() { + return this.#settingsButtonSettings?.labelTexts.map((text) => `a[aria-label="${text}"]`).join(','); } /** * @returns {string} */ - get settingsButtonSelector () { - return `${this.#settingsButtonSettings?.selectors?.join(',')}, ${this.settingsLabelTextSelector}` + get settingsButtonSelector() { + return `${this.#settingsButtonSettings?.selectors?.join(',')}, ${this.settingsLabelTextSelector}`; } - setButtonSettings () { - this.#exportButtonSettings = this.getFeatureSetting('exportButton') - this.#signInButtonSettings = this.getFeatureSetting('signInButton') - this.#settingsButtonSettings = this.getFeatureSetting('settingsButton') + setButtonSettings() { + this.#exportButtonSettings = this.getFeatureSetting('exportButton'); + this.#signInButtonSettings = this.getFeatureSetting('signInButton'); + this.#settingsButtonSettings = this.getFeatureSetting('settingsButton'); } - init () { - this.setButtonSettings() + init() { + this.setButtonSettings(); - const handleElementForPath = this.handleElementForPath.bind(this) + const handleElementForPath = this.handleElementForPath.bind(this); const historyMethodProxy = new DDGProxy(this, History.prototype, 'pushState', { - async apply (target, thisArg, args) { - const path = args[1] === '' ? args[2].split('?')[0] : args[1] - await handleElementForPath(path) - return DDGReflect.apply(target, thisArg, args) - } - }) - historyMethodProxy.overload() + async apply(target, thisArg, args) { + const path = args[1] === '' ? args[2].split('?')[0] : args[1]; + await handleElementForPath(path); + return DDGReflect.apply(target, thisArg, args); + }, + }); + historyMethodProxy.overload(); // listen for popstate events in order to run on back/forward navigations window.addEventListener('popstate', async () => { - await handleElementForPath(window.location.pathname) - }) + await handleElementForPath(window.location.pathname); + }); document.addEventListener('DOMContentLoaded', async () => { - await handleElementForPath(window.location.pathname) - }) + await handleElementForPath(window.location.pathname); + }); } } diff --git a/injected/src/features/breakage-reporting.js b/injected/src/features/breakage-reporting.js index 5b592fc2c..fca601212 100644 --- a/injected/src/features/breakage-reporting.js +++ b/injected/src/features/breakage-reporting.js @@ -1,16 +1,16 @@ -import ContentFeature from '../content-feature' -import { getJsPerformanceMetrics } from './breakage-reporting/utils.js' +import ContentFeature from '../content-feature'; +import { getJsPerformanceMetrics } from './breakage-reporting/utils.js'; export default class BreakageReporting extends ContentFeature { - init () { + init() { this.messaging.subscribe('getBreakageReportValues', () => { - const jsPerformance = getJsPerformanceMetrics() - const referrer = document.referrer + const jsPerformance = getJsPerformanceMetrics(); + const referrer = document.referrer; this.messaging.notify('breakageReportResult', { jsPerformance, - referrer - }) - }) + referrer, + }); + }); } } diff --git a/injected/src/features/breakage-reporting/utils.js b/injected/src/features/breakage-reporting/utils.js index fee549f3b..e1e0776da 100644 --- a/injected/src/features/breakage-reporting/utils.js +++ b/injected/src/features/breakage-reporting/utils.js @@ -1,8 +1,8 @@ /** * @returns array of performance metrics */ -export function getJsPerformanceMetrics () { - const paintResources = performance.getEntriesByType('paint') - const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint') - return firstPaint ? [firstPaint.startTime] : [] +export function getJsPerformanceMetrics() { + const paintResources = performance.getEntriesByType('paint'); + const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint'); + return firstPaint ? [firstPaint.startTime] : []; } diff --git a/injected/src/features/broker-protection.js b/injected/src/features/broker-protection.js index d9e808802..899001fb1 100644 --- a/injected/src/features/broker-protection.js +++ b/injected/src/features/broker-protection.js @@ -1,29 +1,27 @@ -import ContentFeature from '../content-feature.js' -import { execute } from './broker-protection/execute.js' -import { retry } from '../timer-utils.js' +import ContentFeature from '../content-feature.js'; +import { execute } from './broker-protection/execute.js'; +import { retry } from '../timer-utils.js'; /** * @typedef {import("./broker-protection/types.js").ActionResponse} ActionResponse */ export default class BrokerProtection extends ContentFeature { - init () { - this.messaging.subscribe('onActionReceived', async (/** @type {any} */params) => { + init() { + this.messaging.subscribe('onActionReceived', async (/** @type {any} */ params) => { try { - const action = params.state.action - const data = params.state.data + const action = params.state.action; + const data = params.state.data; if (!action) { - return this.messaging.notify('actionError', { error: 'No action found.' }) + return this.messaging.notify('actionError', { error: 'No action found.' }); } /** * Note: We're not currently guarding against concurrent actions here * since the native side contains the scheduling logic to prevent it. */ - let retryConfig = action.retry?.environment === 'web' - ? action.retry - : undefined + let retryConfig = action.retry?.environment === 'web' ? action.retry : undefined; /** * Special case for the exact action @@ -31,33 +29,33 @@ export default class BrokerProtection extends ContentFeature { if (!retryConfig && action.actionType === 'extract') { retryConfig = { interval: { ms: 1000 }, - maxAttempts: 30 - } + maxAttempts: 30, + }; } /** * Special case for when expectation contains a check for an element, retry it */ if (!retryConfig && action.actionType === 'expectation') { - if (action.expectations.some(x => x.type === 'element')) { + if (action.expectations.some((x) => x.type === 'element')) { retryConfig = { interval: { ms: 1000 }, - maxAttempts: 30 - } + maxAttempts: 30, + }; } } - const { result, exceptions } = await retry(() => execute(action, data), retryConfig) + const { result, exceptions } = await retry(() => execute(action, data), retryConfig); if (result) { - this.messaging.notify('actionCompleted', { result }) + this.messaging.notify('actionCompleted', { result }); } else { - this.messaging.notify('actionError', { error: 'No response found, exceptions: ' + exceptions.join(', ') }) + this.messaging.notify('actionError', { error: 'No response found, exceptions: ' + exceptions.join(', ') }); } } catch (e) { - console.log('unhandled exception: ', e) - this.messaging.notify('actionError', { error: e.toString() }) + console.log('unhandled exception: ', e); + this.messaging.notify('actionError', { error: e.toString() }); } - }) + }); } } diff --git a/injected/src/features/broker-protection/actions/build-url-transforms.js b/injected/src/features/broker-protection/actions/build-url-transforms.js index a101b73e5..9bed5d270 100644 --- a/injected/src/features/broker-protection/actions/build-url-transforms.js +++ b/injected/src/features/broker-protection/actions/build-url-transforms.js @@ -1,4 +1,4 @@ -import { getStateFromAbbreviation } from '../comparisons/address.js' +import { getStateFromAbbreviation } from '../comparisons/address.js'; /** * @typedef {{url: string} & Record} BuildUrlAction @@ -13,19 +13,19 @@ import { getStateFromAbbreviation } from '../comparisons/address.js' * @param {Record} userData * @return {{ url: string } | { error: string }} */ -export function transformUrl (action, userData) { - const url = new URL(action.url) +export function transformUrl(action, userData) { + const url = new URL(action.url); /** * assign the updated pathname + search params */ - url.search = processSearchParams(url.searchParams, action, userData).toString() - url.pathname = processPathname(url.pathname, action, userData) + url.search = processSearchParams(url.searchParams, action, userData).toString(); + url.pathname = processPathname(url.pathname, action, userData); /** * Finally, convert back to a full URL */ - return { url: url.toString() } + return { url: url.toString() }; } /** @@ -38,8 +38,8 @@ const baseTransforms = new Map([ ['lastName', (value) => capitalize(value)], ['state', (value) => value.toLowerCase()], ['city', (value) => capitalize(value)], - ['age', (value) => value.toString()] -]) + ['age', (value) => value.toString()], +]); /** * These are optional transforms, will be applied when key is found in the @@ -58,18 +58,20 @@ const optionalTransforms = new Map([ ['snakecase', (value) => value.split(' ').join('_')], ['stateFull', (value) => getStateFromAbbreviation(value)], ['defaultIfEmpty', (value, argument) => value || argument || ''], - ['ageRange', (value, argument, action) => { - if (!action.ageRange) return value - const ageNumber = Number(value) - // find matching age range - const ageRange = action.ageRange.find((range) => { - const [min, max] = range.split('-') - return ageNumber >= Number(min) && ageNumber <= Number(max) - }) - return ageRange || value - } - ] -]) + [ + 'ageRange', + (value, argument, action) => { + if (!action.ageRange) return value; + const ageNumber = Number(value); + // find matching age range + const ageRange = action.ageRange.find((range) => { + const [min, max] = range.split('-'); + return ageNumber >= Number(min) && ageNumber <= Number(max); + }); + return ageRange || value; + }, + ], +]); /** * Take an instance of URLSearchParams and produce a new one, with each variable @@ -80,17 +82,17 @@ const optionalTransforms = new Map([ * @param {Record} userData * @return {URLSearchParams} */ -function processSearchParams (searchParams, action, userData) { +function processSearchParams(searchParams, action, userData) { /** * For each key/value pair in the URL Search params, process the value * part *only*. */ const updatedPairs = [...searchParams].map(([key, value]) => { - const processedValue = processTemplateStringWithUserData(value, action, userData) - return [key, processedValue] - }) + const processedValue = processTemplateStringWithUserData(value, action, userData); + return [key, processedValue]; + }); - return new URLSearchParams(updatedPairs) + return new URLSearchParams(updatedPairs); } /** @@ -98,12 +100,12 @@ function processSearchParams (searchParams, action, userData) { * @param {BuildUrlAction} action * @param {Record} userData */ -function processPathname (pathname, action, userData) { +function processPathname(pathname, action, userData) { return pathname .split('/') .filter(Boolean) - .map(segment => processTemplateStringWithUserData(segment, action, userData)) - .join('/') + .map((segment) => processTemplateStringWithUserData(segment, action, userData)) + .join('/'); } /** @@ -134,17 +136,17 @@ function processPathname (pathname, action, userData) { * @param {BuildUrlAction} action * @param {Record} userData */ -export function processTemplateStringWithUserData (input, action, userData) { +export function processTemplateStringWithUserData(input, action, userData) { /** * Note: this regex covers both pathname + query params. * This is why we're handling both encoded and un-encoded. */ return String(input).replace(/\$%7B(.+?)%7D|\$\{(.+?)}/g, (match, encodedValue, plainValue) => { - const comparison = encodedValue ?? plainValue - const [dataKey, ...transforms] = comparison.split(/\||%7C/) - const data = userData[dataKey] - return applyTransforms(dataKey, data, transforms, action) - }) + const comparison = encodedValue ?? plainValue; + const [dataKey, ...transforms] = comparison.split(/\||%7C/); + const data = userData[dataKey]; + return applyTransforms(dataKey, data, transforms, action); + }); } /** @@ -153,28 +155,26 @@ export function processTemplateStringWithUserData (input, action, userData) { * @param {string[]} transformNames * @param {BuildUrlAction} action */ -function applyTransforms (dataKey, value, transformNames, action) { - const subject = String(value || '') - const baseTransform = baseTransforms.get(dataKey) +function applyTransforms(dataKey, value, transformNames, action) { + const subject = String(value || ''); + const baseTransform = baseTransforms.get(dataKey); // apply base transform to the incoming string - let outputString = baseTransform - ? baseTransform(subject) - : subject + let outputString = baseTransform ? baseTransform(subject) : subject; for (const transformName of transformNames) { - const [name, argument] = transformName.split(':') - const transform = optionalTransforms.get(name) + const [name, argument] = transformName.split(':'); + const transform = optionalTransforms.get(name); if (transform) { - outputString = transform(outputString, argument, action) + outputString = transform(outputString, argument, action); } } - return outputString + return outputString; } -function capitalize (s) { - const words = s.split(' ') - const capitalizedWords = words.map(word => word.charAt(0).toUpperCase() + word.slice(1)) - return capitalizedWords.join(' ') +function capitalize(s) { + const words = s.split(' '); + const capitalizedWords = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)); + return capitalizedWords.join(' '); } diff --git a/injected/src/features/broker-protection/actions/build-url.js b/injected/src/features/broker-protection/actions/build-url.js index f696c4077..5a8333309 100644 --- a/injected/src/features/broker-protection/actions/build-url.js +++ b/injected/src/features/broker-protection/actions/build-url.js @@ -1,5 +1,5 @@ -import { transformUrl } from './build-url-transforms.js' -import { ErrorResponse, SuccessResponse } from '../types.js' +import { transformUrl } from './build-url-transforms.js'; +import { ErrorResponse, SuccessResponse } from '../types.js'; /** * This builds the proper URL given the URL template and userData. @@ -8,13 +8,13 @@ import { ErrorResponse, SuccessResponse } from '../types.js' * @param {Record} userData * @return {import('../types.js').ActionResponse} */ -export function buildUrl (action, userData) { - const result = replaceTemplatedUrl(action, userData) +export function buildUrl(action, userData) { + const result = replaceTemplatedUrl(action, userData); if ('error' in result) { - return new ErrorResponse({ actionID: action.id, message: result.error }) + return new ErrorResponse({ actionID: action.id, message: result.error }); } - return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: { url: result.url } }) + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: { url: result.url } }); } /** @@ -24,22 +24,22 @@ export function buildUrl (action, userData) { * @param userData * @return {{url: string} | {error: string}} */ -export function replaceTemplatedUrl (action, userData) { - const url = action?.url +export function replaceTemplatedUrl(action, userData) { + const url = action?.url; if (!url) { - return { error: 'Error: No url provided.' } + return { error: 'Error: No url provided.' }; } try { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _ = new URL(action.url) + const _ = new URL(action.url); } catch (e) { - return { error: 'Error: Invalid URL provided.' } + return { error: 'Error: Invalid URL provided.' }; } if (!userData) { - return { url } + return { url }; } - return transformUrl(action, userData) + return transformUrl(action, userData); } diff --git a/injected/src/features/broker-protection/actions/captcha-callback.js b/injected/src/features/broker-protection/actions/captcha-callback.js index d3d1a64cc..f44859087 100644 --- a/injected/src/features/broker-protection/actions/captcha-callback.js +++ b/injected/src/features/broker-protection/actions/captcha-callback.js @@ -2,20 +2,20 @@ * @param {object} args * @param {string} args.token */ -export function captchaCallback (args) { - const clients = findRecaptchaClients(globalThis) +export function captchaCallback(args) { + const clients = findRecaptchaClients(globalThis); // if a client was found, check there was a function if (clients.length === 0) { - return console.log('cannot find clients') + return console.log('cannot find clients'); } if (typeof clients[0].function === 'function') { try { - clients[0].function(args.token) - console.log('called function with path', clients[0].callback) + clients[0].function(args.token); + console.log('called function with path', clients[0].callback); } catch (e) { - console.error('could not call function') + console.error('could not call function'); } } @@ -23,47 +23,49 @@ export function captchaCallback (args) { * Try to find a callback in a path such as ['___grecaptcha_cfg', 'clients', '0', 'U', 'U', 'callback'] * @param {Record} target */ - function findRecaptchaClients (target) { - if (typeof (target.___grecaptcha_cfg) === 'undefined') { - console.log('target.___grecaptcha_cfg not found in ', location.href) - return [] + function findRecaptchaClients(target) { + if (typeof target.___grecaptcha_cfg === 'undefined') { + console.log('target.___grecaptcha_cfg not found in ', location.href); + return []; } return Object.entries(target.___grecaptcha_cfg.clients || {}).map(([cid, client]) => { - const cidNumber = parseInt(cid, 10) + const cidNumber = parseInt(cid, 10); const data = { id: cid, - version: cidNumber >= 10000 ? 'V3' : 'V2' - } - const objects = Object.entries(client).filter(([, value]) => value && typeof value === 'object') + version: cidNumber >= 10000 ? 'V3' : 'V2', + }; + const objects = Object.entries(client).filter(([, value]) => value && typeof value === 'object'); objects.forEach(([toplevelKey, toplevel]) => { - const found = Object.entries(toplevel).find(([, value]) => ( - value && typeof value === 'object' && 'sitekey' in value && 'size' in value - )) + const found = Object.entries(toplevel).find( + ([, value]) => value && typeof value === 'object' && 'sitekey' in value && 'size' in value, + ); - if (typeof toplevel === 'object' && + if ( + typeof toplevel === 'object' && typeof HTMLElement !== 'undefined' && toplevel instanceof HTMLElement && - toplevel.tagName === 'DIV') { - data.pageurl = toplevel.baseURI + toplevel.tagName === 'DIV' + ) { + data.pageurl = toplevel.baseURI; } if (found) { - const [sublevelKey, sublevel] = found + const [sublevelKey, sublevel] = found; - data.sitekey = sublevel.sitekey - const callbackKey = data.version === 'V2' ? 'callback' : 'promise-callback' - const callback = sublevel[callbackKey] + data.sitekey = sublevel.sitekey; + const callbackKey = data.version === 'V2' ? 'callback' : 'promise-callback'; + const callback = sublevel[callbackKey]; if (!callback) { - data.callback = null - data.function = null + data.callback = null; + data.function = null; } else { - data.function = callback - data.callback = ['___grecaptcha_cfg', 'clients', cid, toplevelKey, sublevelKey, callbackKey] + data.function = callback; + data.callback = ['___grecaptcha_cfg', 'clients', cid, toplevelKey, sublevelKey, callbackKey]; } } - }) - return data - }) + }); + return data; + }); } } diff --git a/injected/src/features/broker-protection/actions/captcha.js b/injected/src/features/broker-protection/actions/captcha.js index 64312ed0e..48e285573 100644 --- a/injected/src/features/broker-protection/actions/captcha.js +++ b/injected/src/features/broker-protection/actions/captcha.js @@ -1,6 +1,6 @@ -import { captchaCallback } from './captcha-callback.js' -import { getElement } from '../utils.js' -import { ErrorResponse, SuccessResponse } from '../types.js' +import { captchaCallback } from './captcha-callback.js'; +import { getElement } from '../utils.js'; +import { ErrorResponse, SuccessResponse } from '../types.js'; /** * Gets the captcha information to send to the backend @@ -9,109 +9,111 @@ import { ErrorResponse, SuccessResponse } from '../types.js' * @param {Document | HTMLElement} root * @return {import('../types.js').ActionResponse} */ -export function getCaptchaInfo (action, root = document) { - const pageUrl = window.location.href - const captchaDiv = getElement(root, action.selector) +export function getCaptchaInfo(action, root = document) { + const pageUrl = window.location.href; + const captchaDiv = getElement(root, action.selector); // if 'captchaDiv' was missing, cannot continue - if (!captchaDiv) return new ErrorResponse({ actionID: action.id, message: `could not find captchaDiv with selector ${action.selector}` }) + if (!captchaDiv) + return new ErrorResponse({ actionID: action.id, message: `could not find captchaDiv with selector ${action.selector}` }); // try 2 different captures - const captcha = getElement(captchaDiv, '[src^="https://www.google.com/recaptcha"]') || - getElement(captchaDiv, '[src^="https://newassets.hcaptcha.com/captcha"') + const captcha = + getElement(captchaDiv, '[src^="https://www.google.com/recaptcha"]') || + getElement(captchaDiv, '[src^="https://newassets.hcaptcha.com/captcha"'); // ensure we have the elements - if (!captcha) return new ErrorResponse({ actionID: action.id, message: 'could not find captcha' }) - if (!('src' in captcha)) return new ErrorResponse({ actionID: action.id, message: 'missing src attribute' }) + if (!captcha) return new ErrorResponse({ actionID: action.id, message: 'could not find captcha' }); + if (!('src' in captcha)) return new ErrorResponse({ actionID: action.id, message: 'missing src attribute' }); - const captchaUrl = String(captcha.src) - let captchaType - let siteKey + const captchaUrl = String(captcha.src); + let captchaType; + let siteKey; if (captchaUrl.includes('recaptcha/api2')) { - captchaType = 'recaptcha2' - siteKey = new URL(captchaUrl).searchParams.get('k') + captchaType = 'recaptcha2'; + siteKey = new URL(captchaUrl).searchParams.get('k'); } else if (captchaUrl.includes('recaptcha/enterprise')) { - captchaType = 'recaptchaEnterprise' - siteKey = new URL(captchaUrl).searchParams.get('k') + captchaType = 'recaptchaEnterprise'; + siteKey = new URL(captchaUrl).searchParams.get('k'); } else if (captchaUrl.includes('hcaptcha.com/captcha/v1')) { - captchaType = 'hcaptcha' + captchaType = 'hcaptcha'; // hcaptcha sitekey may be in either if (captcha instanceof Element) { - siteKey = captcha.getAttribute('data-sitekey') + siteKey = captcha.getAttribute('data-sitekey'); } if (!siteKey) { try { // `new URL(...)` can throw, so it's valid to wrap this in try/catch - siteKey = new URL(captchaUrl).searchParams.get('sitekey') + siteKey = new URL(captchaUrl).searchParams.get('sitekey'); } catch (e) { - console.warn('error parsing captchaUrl', captchaUrl) + console.warn('error parsing captchaUrl', captchaUrl); } } if (!siteKey) { try { - const hash = new URL(captchaUrl).hash.slice(1) - siteKey = new URLSearchParams(hash).get('sitekey') + const hash = new URL(captchaUrl).hash.slice(1); + siteKey = new URLSearchParams(hash).get('sitekey'); } catch (e) { - console.warn('error parsing captchaUrl hash', captchaUrl) + console.warn('error parsing captchaUrl hash', captchaUrl); } } } if (!captchaType) { - return new ErrorResponse({ actionID: action.id, message: 'Could not extract captchaType.' }) + return new ErrorResponse({ actionID: action.id, message: 'Could not extract captchaType.' }); } if (!siteKey) { - return new ErrorResponse({ actionID: action.id, message: 'Could not extract siteKey.' }) + return new ErrorResponse({ actionID: action.id, message: 'Could not extract siteKey.' }); } // Remove query params (which may include PII) - const pageUrlWithoutParams = pageUrl?.split('?')[0] + const pageUrlWithoutParams = pageUrl?.split('?')[0]; const responseData = { siteKey, url: pageUrlWithoutParams, - type: captchaType - } + type: captchaType, + }; - return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: responseData }) + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: responseData }); } /** -* Takes the solved captcha token and injects it into the page to solve the captcha -* -* @param action -* @param {string} token -* @param {Document} root -* @return {import('../types.js').ActionResponse} -*/ -export function solveCaptcha (action, token, root = document) { - const selectors = ['h-captcha-response', 'g-recaptcha-response'] - let solved = false + * Takes the solved captcha token and injects it into the page to solve the captcha + * + * @param action + * @param {string} token + * @param {Document} root + * @return {import('../types.js').ActionResponse} + */ +export function solveCaptcha(action, token, root = document) { + const selectors = ['h-captcha-response', 'g-recaptcha-response']; + let solved = false; for (const selector of selectors) { - const match = root.getElementsByName(selector)[0] + const match = root.getElementsByName(selector)[0]; if (match) { - match.innerHTML = token - solved = true - break + match.innerHTML = token; + solved = true; + break; } } if (solved) { - const json = JSON.stringify({ token }) + const json = JSON.stringify({ token }); const javascript = `;(function(args) { ${captchaCallback.toString()}; captchaCallback(args); - })(${json});` + })(${json});`; return new SuccessResponse({ actionID: action.id, actionType: action.actionType, - response: { callback: { eval: javascript } } - }) + response: { callback: { eval: javascript } }, + }); } - return new ErrorResponse({ actionID: action.id, message: 'could not solve captcha' }) + return new ErrorResponse({ actionID: action.id, message: 'could not solve captcha' }); } diff --git a/injected/src/features/broker-protection/actions/click.js b/injected/src/features/broker-protection/actions/click.js index 6c8b89ea9..75a7197c8 100644 --- a/injected/src/features/broker-protection/actions/click.js +++ b/injected/src/features/broker-protection/actions/click.js @@ -1,6 +1,6 @@ -import { getElements } from '../utils.js' -import { ErrorResponse, SuccessResponse } from '../types.js' -import { extractProfiles } from './extract.js' +import { getElements } from '../utils.js'; +import { ErrorResponse, SuccessResponse } from '../types.js'; +import { extractProfiles } from './extract.js'; /** * @param {Record} action @@ -8,33 +8,36 @@ import { extractProfiles } from './extract.js' * @param {Document | HTMLElement} root * @return {import('../types.js').ActionResponse} */ -export function click (action, userData, root = document) { +export function click(action, userData, root = document) { // there can be multiple elements provided by the action for (const element of action.elements) { - const rootElement = selectRootElement(element, userData, root) - const elements = getElements(rootElement, element.selector) + const rootElement = selectRootElement(element, userData, root); + const elements = getElements(rootElement, element.selector); if (!elements?.length) { - return new ErrorResponse({ actionID: action.id, message: `could not find element to click with selector '${element.selector}'!` }) + return new ErrorResponse({ + actionID: action.id, + message: `could not find element to click with selector '${element.selector}'!`, + }); } - const loopLength = element.multiple && element.multiple === true ? elements.length : 1 + const loopLength = element.multiple && element.multiple === true ? elements.length : 1; for (let i = 0; i < loopLength; i++) { - const elem = elements[i] + const elem = elements[i]; if ('disabled' in elem) { if (elem.disabled) { - return new ErrorResponse({ actionID: action.id, message: `could not click disabled element ${element.selector}'!` }) + return new ErrorResponse({ actionID: action.id, message: `could not click disabled element ${element.selector}'!` }); } } if ('click' in elem && typeof elem.click === 'function') { - elem.click() + elem.click(); } } } - return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }) + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }); } /** @@ -43,23 +46,21 @@ export function click (action, userData, root = document) { * @param {Document | HTMLElement} root * @return {Node} */ -function selectRootElement (clickElement, userData, root = document) { +function selectRootElement(clickElement, userData, root = document) { // if there's no 'parent' field, just use the document - if (!clickElement.parent) return root + if (!clickElement.parent) return root; // if the 'parent' field contains 'profileMatch', try to match it if (clickElement.parent.profileMatch) { - const extraction = extractProfiles(clickElement.parent.profileMatch, userData, root) + const extraction = extractProfiles(clickElement.parent.profileMatch, userData, root); if ('results' in extraction) { - const sorted = extraction.results - .filter(x => x.result === true) - .sort((a, b) => b.score - a.score) - const first = sorted[0] + const sorted = extraction.results.filter((x) => x.result === true).sort((a, b) => b.score - a.score); + const first = sorted[0]; if (first && first.element) { - return first.element + return first.element; } } } - throw new Error('`parent` was present on the element, but the configuration is not supported') + throw new Error('`parent` was present on the element, but the configuration is not supported'); } diff --git a/injected/src/features/broker-protection/actions/expectation.js b/injected/src/features/broker-protection/actions/expectation.js index 29a5531d1..cbdd4bf2c 100644 --- a/injected/src/features/broker-protection/actions/expectation.js +++ b/injected/src/features/broker-protection/actions/expectation.js @@ -1,6 +1,6 @@ -import { getElement } from '../utils.js' -import { ErrorResponse, SuccessResponse } from '../types.js' -import { execute } from '../execute.js' +import { getElement } from '../utils.js'; +import { ErrorResponse, SuccessResponse } from '../types.js'; +import { execute } from '../execute.js'; /** * @param {Record} action @@ -8,42 +8,43 @@ import { execute } from '../execute.js' * @param {Document} root * @return {Promise} */ -export async function expectation (action, userData, root = document) { - const results = expectMany(action.expectations, root) +export async function expectation(action, userData, root = document) { + const results = expectMany(action.expectations, root); // filter out good results + silent failures, leaving only fatal errors const errors = results .filter((x, index) => { - if (x.result === true) return false - if (action.expectations[index].failSilently) return false - return true - }).map((x) => { - return 'error' in x ? x.error : 'unknown error' + if (x.result === true) return false; + if (action.expectations[index].failSilently) return false; + return true; }) + .map((x) => { + return 'error' in x ? x.error : 'unknown error'; + }); if (errors.length > 0) { - return new ErrorResponse({ actionID: action.id, message: errors.join(', ') }) + return new ErrorResponse({ actionID: action.id, message: errors.join(', ') }); } // only run later actions if every expectation was met - const runActions = results.every(x => x.result === true) - const secondaryErrors = [] + const runActions = results.every((x) => x.result === true); + const secondaryErrors = []; if (action.actions?.length && runActions) { for (const subAction of action.actions) { - const result = await execute(subAction, userData, root) + const result = await execute(subAction, userData, root); if ('error' in result) { - secondaryErrors.push(result.error) + secondaryErrors.push(result.error); } } if (secondaryErrors.length > 0) { - return new ErrorResponse({ actionID: action.id, message: secondaryErrors.join(', ') }) + return new ErrorResponse({ actionID: action.id, message: secondaryErrors.join(', ') }); } } - return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }) + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }); } /** @@ -53,20 +54,23 @@ export async function expectation (action, userData, root = document) { * @param {Document | HTMLElement} root * @return {import("../types").BooleanResult[]} */ -export function expectMany (expectations, root) { - return expectations.map(expectation => { +export function expectMany(expectations, root) { + return expectations.map((expectation) => { switch (expectation.type) { - case 'element': return elementExpectation(expectation, root) - case 'text': return textExpectation(expectation, root) - case 'url': return urlExpectation(expectation) - default: { - return { - result: false, - error: `unknown expectation type: ${expectation.type}` + case 'element': + return elementExpectation(expectation, root); + case 'text': + return textExpectation(expectation, root); + case 'url': + return urlExpectation(expectation); + default: { + return { + result: false, + error: `unknown expectation type: ${expectation.type}`, + }; } } - } - }) + }); } /** @@ -77,27 +81,27 @@ export function expectMany (expectations, root) { * @param {Document | HTMLElement} root * @return {import("../types").BooleanResult} */ -export function elementExpectation (expectation, root) { +export function elementExpectation(expectation, root) { if (expectation.parent) { - const parent = getElement(root, expectation.parent) + const parent = getElement(root, expectation.parent); if (!parent) { return { result: false, - error: `parent element not found with selector: ${expectation.parent}` - } + error: `parent element not found with selector: ${expectation.parent}`, + }; } - parent.scrollIntoView() + parent.scrollIntoView(); } - const elementExists = getElement(root, expectation.selector) !== null + const elementExists = getElement(root, expectation.selector) !== null; if (!elementExists) { return { result: false, - error: `element with selector ${expectation.selector} not found.` - } + error: `element with selector ${expectation.selector} not found.`, + }; } - return { result: true } + return { result: true }; } /** @@ -107,35 +111,35 @@ export function elementExpectation (expectation, root) { * @param {Document | HTMLElement} root * @return {import("../types").BooleanResult} */ -export function textExpectation (expectation, root) { +export function textExpectation(expectation, root) { // get the target element first - const elem = getElement(root, expectation.selector) + const elem = getElement(root, expectation.selector); if (!elem) { return { result: false, - error: `element with selector ${expectation.selector} not found.` - } + error: `element with selector ${expectation.selector} not found.`, + }; } // todo: remove once we have stronger types if (!expectation.expect) { return { result: false, - error: 'missing key: \'expect\'' - } + error: "missing key: 'expect'", + }; } // todo: is this too strict a match? we may also want to try innerText - const textExists = Boolean(elem?.textContent?.includes(expectation.expect)) + const textExists = Boolean(elem?.textContent?.includes(expectation.expect)); if (!textExists) { return { result: false, - error: `expected element with selector ${expectation.selector} to have text: ${expectation.expect}, but it didn't` - } + error: `expected element with selector ${expectation.selector} to have text: ${expectation.expect}, but it didn't`, + }; } - return { result: true } + return { result: true }; } /** @@ -144,23 +148,23 @@ export function textExpectation (expectation, root) { * @param {import("../types").Expectation} expectation * @return {import("../types").BooleanResult} */ -export function urlExpectation (expectation) { - const url = window.location.href +export function urlExpectation(expectation) { + const url = window.location.href; // todo: remove once we have stronger types if (!expectation.expect) { return { result: false, - error: 'missing key: \'expect\'' - } + error: "missing key: 'expect'", + }; } if (!url.includes(expectation.expect)) { return { result: false, - error: `expected URL to include ${expectation.expect}, but it didn't` - } + error: `expected URL to include ${expectation.expect}, but it didn't`, + }; } - return { result: true } + return { result: true }; } diff --git a/injected/src/features/broker-protection/actions/extract.js b/injected/src/features/broker-protection/actions/extract.js index 6d84ae526..39df1e95c 100644 --- a/injected/src/features/broker-protection/actions/extract.js +++ b/injected/src/features/broker-protection/actions/extract.js @@ -1,24 +1,14 @@ -import { - cleanArray, - getElement, - getElementMatches, - getElements, - sortAddressesByStateAndCity -} from '../utils.js' // Assuming you have imported the address comparison function -import { - ErrorResponse, - ProfileResult, - SuccessResponse -} from '../types.js' -import { isSameAge } from '../comparisons/is-same-age.js' -import { isSameName } from '../comparisons/is-same-name.js' -import { addressMatch } from '../comparisons/address.js' -import { AgeExtractor } from '../extractors/age.js' -import { AlternativeNamesExtractor, NameExtractor } from '../extractors/name.js' -import { AddressFullExtractor, CityStateExtractor } from '../extractors/address.js' -import { PhoneExtractor } from '../extractors/phone.js' -import { RelativesExtractor } from '../extractors/relatives.js' -import { ProfileHashTransformer, ProfileUrlExtractor } from '../extractors/profile-url.js' +import { cleanArray, getElement, getElementMatches, getElements, sortAddressesByStateAndCity } from '../utils.js'; // Assuming you have imported the address comparison function +import { ErrorResponse, ProfileResult, SuccessResponse } from '../types.js'; +import { isSameAge } from '../comparisons/is-same-age.js'; +import { isSameName } from '../comparisons/is-same-name.js'; +import { addressMatch } from '../comparisons/address.js'; +import { AgeExtractor } from '../extractors/age.js'; +import { AlternativeNamesExtractor, NameExtractor } from '../extractors/name.js'; +import { AddressFullExtractor, CityStateExtractor } from '../extractors/address.js'; +import { PhoneExtractor } from '../extractors/phone.js'; +import { RelativesExtractor } from '../extractors/relatives.js'; +import { ProfileHashTransformer, ProfileUrlExtractor } from '../extractors/profile-url.js'; /** * Adding these types here so that we can switch to generated ones later @@ -48,23 +38,23 @@ import { ProfileHashTransformer, ProfileUrlExtractor } from '../extractors/profi * @param {Document | HTMLElement} root * @return {Promise} */ -export async function extract (action, userData, root = document) { - const extractResult = extractProfiles(action, userData, root) +export async function extract(action, userData, root = document) { + const extractResult = extractProfiles(action, userData, root); if ('error' in extractResult) { - return new ErrorResponse({ actionID: action.id, message: extractResult.error }) + return new ErrorResponse({ actionID: action.id, message: extractResult.error }); } const filteredPromises = extractResult.results - .filter(x => x.result === true) - .map(x => aggregateFields(x.scrapedData)) - .map(profile => applyPostTransforms(profile, action.profile)) + .filter((x) => x.result === true) + .map((x) => aggregateFields(x.scrapedData)) + .map((profile) => applyPostTransforms(profile, action.profile)); - const filtered = await Promise.all(filteredPromises) + const filtered = await Promise.all(filteredPromises); // omit the DOM node from data transfer - - const debugResults = extractResult.results.map((result) => result.asData()) + + const debugResults = extractResult.results.map((result) => result.asData()); return new SuccessResponse({ actionID: action.id, @@ -72,9 +62,9 @@ export async function extract (action, userData, root = document) { response: filtered, meta: { userData, - extractResults: debugResults - } - }) + extractResults: debugResults, + }, + }); } /** @@ -83,19 +73,19 @@ export async function extract (action, userData, root = document) { * @param {Element | Document} [root] * @return {{error: string} | {results: ProfileResult[]}} */ -export function extractProfiles (action, userData, root = document) { - const profilesElementList = getElements(root, action.selector) ?? [] +export function extractProfiles(action, userData, root = document) { + const profilesElementList = getElements(root, action.selector) ?? []; if (profilesElementList.length === 0) { if (!action.noResultsSelector) { - return { error: 'no root elements found for ' + action.selector } + return { error: 'no root elements found for ' + action.selector }; } // Look for the Results Not Found element - const foundNoResultsElement = getElement(root, action.noResultsSelector) + const foundNoResultsElement = getElement(root, action.noResultsSelector); if (!foundNoResultsElement) { - return { error: 'no results found for ' + action.selector + ' or the no results selector ' + action.noResultsSelector } + return { error: 'no results found for ' + action.selector + ' or the no results selector ' + action.noResultsSelector }; } } @@ -104,19 +94,19 @@ export function extractProfiles (action, userData, root = document) { const elementFactory = (key, value) => { return value?.findElements ? cleanArray(getElements(element, value.selector)) - : cleanArray(getElement(element, value.selector) || getElementMatches(element, value.selector)) - } - const scrapedData = createProfile(elementFactory, action.profile) - const { result, score, matchedFields } = scrapedDataMatchesUserData(userData, scrapedData) + : cleanArray(getElement(element, value.selector) || getElementMatches(element, value.selector)); + }; + const scrapedData = createProfile(elementFactory, action.profile); + const { result, score, matchedFields } = scrapedDataMatchesUserData(userData, scrapedData); return new ProfileResult({ scrapedData, result, score, element, - matchedFields - }) - }) - } + matchedFields, + }); + }), + }; } /** @@ -144,29 +134,29 @@ export function extractProfiles (action, userData, root = document) { * @param {Record} extractData * @return {Record} */ -export function createProfile (elementFactory, extractData) { - const output = {} +export function createProfile(elementFactory, extractData) { + const output = {}; for (const [key, value] of Object.entries(extractData)) { if (!value?.selector) { - output[key] = null + output[key] = null; } else { - const elements = elementFactory(key, value) + const elements = elementFactory(key, value); // extract all strings first - const evaluatedValues = stringValuesFromElements(elements, key, value) + const evaluatedValues = stringValuesFromElements(elements, key, value); // clean them up - trimming, removing empties - const noneEmptyArray = cleanArray(evaluatedValues) + const noneEmptyArray = cleanArray(evaluatedValues); // Note: This can return any valid JSON valid, it depends on the extractor used. - const extractedValue = extractValue(key, value, noneEmptyArray) + const extractedValue = extractValue(key, value, noneEmptyArray); // try to use the extracted value, or fall back to null // this allows 'extractValue' to return null|undefined - output[key] = extractedValue || null + output[key] = extractedValue || null; } } - return output + return output; } /** @@ -175,23 +165,23 @@ export function createProfile (elementFactory, extractData) { * @param {ExtractProfileProperty} extractField * @return {string[]} */ -function stringValuesFromElements (elements, key, extractField) { - return elements.map(element => { +function stringValuesFromElements(elements, key, extractField) { + return elements.map((element) => { // todo: should we use textContent here? - let elementValue = rules[key]?.(element) ?? element?.innerText ?? null + let elementValue = rules[key]?.(element) ?? element?.innerText ?? null; if (extractField?.afterText) { - elementValue = elementValue?.split(extractField.afterText)[1]?.trim() || elementValue + elementValue = elementValue?.split(extractField.afterText)[1]?.trim() || elementValue; } // there is a case where we may want to get the text "after" and "before" certain text if (extractField?.beforeText) { - elementValue = elementValue?.split(extractField.beforeText)[0].trim() || elementValue + elementValue = elementValue?.split(extractField.beforeText)[0].trim() || elementValue; } - elementValue = removeCommonSuffixesAndPrefixes(elementValue) + elementValue = removeCommonSuffixesAndPrefixes(elementValue); - return elementValue - }) + return elementValue; + }); } /** @@ -200,76 +190,71 @@ function stringValuesFromElements (elements, key, extractField) { * @param {Record} scrapedData * @return {{score: number, matchedFields: string[], result: boolean}} */ -export function scrapedDataMatchesUserData (userData, scrapedData) { - const matchedFields = [] +export function scrapedDataMatchesUserData(userData, scrapedData) { + const matchedFields = []; // the name matching is always a *requirement* if (isSameName(scrapedData.name, userData.firstName, userData.middleName, userData.lastName)) { - matchedFields.push('name') + matchedFields.push('name'); } else { - return { matchedFields, score: matchedFields.length, result: false } + return { matchedFields, score: matchedFields.length, result: false }; } // if the age field was present in the scraped data, then we consider this check a *requirement* if (scrapedData.age) { if (isSameAge(scrapedData.age, userData.age)) { - matchedFields.push('age') + matchedFields.push('age'); } else { - return { matchedFields, score: matchedFields.length, result: false } + return { matchedFields, score: matchedFields.length, result: false }; } } - const addressFields = [ - 'addressCityState', - 'addressCityStateList', - 'addressFull', - 'addressFullList' - ] + const addressFields = ['addressCityState', 'addressCityStateList', 'addressFull', 'addressFullList']; for (const addressField of addressFields) { if (addressField in scrapedData) { if (addressMatch(userData.addresses, scrapedData[addressField])) { - matchedFields.push(addressField) - return { matchedFields, score: matchedFields.length, result: true } + matchedFields.push(addressField); + return { matchedFields, score: matchedFields.length, result: true }; } } } if (scrapedData.phone) { if (userData.phone === scrapedData.phone) { - matchedFields.push('phone') - return { matchedFields, score: matchedFields.length, result: true } + matchedFields.push('phone'); + return { matchedFields, score: matchedFields.length, result: true }; } } // if we get here we didn't consider it a match - return { matchedFields, score: matchedFields.length, result: false } + return { matchedFields, score: matchedFields.length, result: false }; } /** * @param {Record} profile */ -export function aggregateFields (profile) { +export function aggregateFields(profile) { // addresses const combinedAddresses = [ - ...profile.addressCityState || [], - ...profile.addressCityStateList || [], - ...profile.addressFullList || [], - ...profile.addressFull || [] - ] - const addressMap = new Map(combinedAddresses.map(addr => [`${addr.city},${addr.state}`, addr])) - const addresses = sortAddressesByStateAndCity([...addressMap.values()]) + ...(profile.addressCityState || []), + ...(profile.addressCityStateList || []), + ...(profile.addressFullList || []), + ...(profile.addressFull || []), + ]; + const addressMap = new Map(combinedAddresses.map((addr) => [`${addr.city},${addr.state}`, addr])); + const addresses = sortAddressesByStateAndCity([...addressMap.values()]); // phone - const phoneArray = profile.phone || [] - const phoneListArray = profile.phoneList || [] - const phoneNumbers = [...new Set([...phoneArray, ...phoneListArray])].sort((a, b) => parseInt(a) - parseInt(b)) + const phoneArray = profile.phone || []; + const phoneListArray = profile.phoneList || []; + const phoneNumbers = [...new Set([...phoneArray, ...phoneListArray])].sort((a, b) => parseInt(a) - parseInt(b)); // relatives - const relatives = [...new Set(profile.relativesList)].sort() + const relatives = [...new Set(profile.relativesList)].sort(); // aliases - const alternativeNames = [...new Set(profile.alternativeNamesList)].sort() + const alternativeNames = [...new Set(profile.alternativeNamesList)].sort(); return { name: profile.name, @@ -278,8 +263,8 @@ export function aggregateFields (profile) { addresses, phoneNumbers, relatives, - ...profile.profileUrl - } + ...profile.profileUrl, + }; } /** @@ -300,27 +285,32 @@ export function aggregateFields (profile) { * @param {string[]} elementValues * @return {any} */ -export function extractValue (outputFieldKey, extractorParams, elementValues) { +export function extractValue(outputFieldKey, extractorParams, elementValues) { switch (outputFieldKey) { - case 'age': return new AgeExtractor().extract(elementValues, extractorParams) - case 'name': return new NameExtractor().extract(elementValues, extractorParams) - - // all addresses are processed the same way - case 'addressFull': - case 'addressFullList': - return new AddressFullExtractor().extract(elementValues, extractorParams) - case 'addressCityState': - case 'addressCityStateList': - return new CityStateExtractor().extract(elementValues, extractorParams) - - case 'alternativeNamesList': return new AlternativeNamesExtractor().extract(elementValues, extractorParams) - case 'relativesList': return new RelativesExtractor().extract(elementValues, extractorParams) - case 'phone': - case 'phoneList': - return new PhoneExtractor().extract(elementValues, extractorParams) - case 'profileUrl': return new ProfileUrlExtractor().extract(elementValues, extractorParams) + case 'age': + return new AgeExtractor().extract(elementValues, extractorParams); + case 'name': + return new NameExtractor().extract(elementValues, extractorParams); + + // all addresses are processed the same way + case 'addressFull': + case 'addressFullList': + return new AddressFullExtractor().extract(elementValues, extractorParams); + case 'addressCityState': + case 'addressCityStateList': + return new CityStateExtractor().extract(elementValues, extractorParams); + + case 'alternativeNamesList': + return new AlternativeNamesExtractor().extract(elementValues, extractorParams); + case 'relativesList': + return new RelativesExtractor().extract(elementValues, extractorParams); + case 'phone': + case 'phoneList': + return new PhoneExtractor().extract(elementValues, extractorParams); + case 'profileUrl': + return new ProfileUrlExtractor().extract(elementValues, extractorParams); } - return null + return null; } /** @@ -330,19 +320,19 @@ export function extractValue (outputFieldKey, extractorParams, elementValues) { * @param {Record} params * @return {Promise>} */ -async function applyPostTransforms (profile, params) { +async function applyPostTransforms(profile, params) { /** @type {import("../types.js").AsyncProfileTransform[]} */ const transforms = [ // creates a hash if needed - new ProfileHashTransformer() - ] + new ProfileHashTransformer(), + ]; - let output = profile + let output = profile; for (const knownTransform of transforms) { - output = await knownTransform.transform(output, params) + output = await knownTransform.transform(output, params); } - return output + return output; } /** @@ -350,17 +340,17 @@ async function applyPostTransforms (profile, params) { * @param {string} [separator] * @return {string[]} */ -export function stringToList (inputList, separator) { - const defaultSeparator = /[|\n•·]/ - return cleanArray(inputList.split(separator || defaultSeparator)) +export function stringToList(inputList, separator) { + const defaultSeparator = /[|\n•·]/; + return cleanArray(inputList.split(separator || defaultSeparator)); } // For extraction const rules = { profileUrl: function (link) { - return link?.href ?? null - } -} + return link?.href ?? null; + }, +}; /** * Remove common prefixes and suffixes such as @@ -372,11 +362,11 @@ const rules = { * @param {string} elementValue * @return {string} */ -function removeCommonSuffixesAndPrefixes (elementValue) { +function removeCommonSuffixesAndPrefixes(elementValue) { const regexes = [ // match text such as +3 more when it appears at the end of a string - /\+\s*\d+.*$/ - ] + /\+\s*\d+.*$/, + ]; // strings that are always safe to remove from the start const startsWith = [ 'Associated persons:', @@ -389,28 +379,25 @@ function removeCommonSuffixesAndPrefixes (elementValue) { 'Lives in:', 'Related to:', 'No other aliases.', - 'RESIDES IN' - ] + 'RESIDES IN', + ]; // strings that are always safe to remove from the end - const endsWith = [ - ' -', - 'years old' - ] + const endsWith = [' -', 'years old']; for (const regex of regexes) { - elementValue = elementValue.replace(regex, '').trim() + elementValue = elementValue.replace(regex, '').trim(); } for (const prefix of startsWith) { if (elementValue.startsWith(prefix)) { - elementValue = elementValue.slice(prefix.length).trim() + elementValue = elementValue.slice(prefix.length).trim(); } } for (const suffix of endsWith) { if (elementValue.endsWith(suffix)) { - elementValue = elementValue.slice(0, 0 - (suffix.length)).trim() + elementValue = elementValue.slice(0, 0 - suffix.length).trim(); } } - return elementValue + return elementValue; } diff --git a/injected/src/features/broker-protection/actions/fill-form.js b/injected/src/features/broker-protection/actions/fill-form.js index 91198f985..28bf5bf78 100644 --- a/injected/src/features/broker-protection/actions/fill-form.js +++ b/injected/src/features/broker-protection/actions/fill-form.js @@ -1,6 +1,6 @@ -import { getElement, generateRandomInt } from '../utils.js' -import { ErrorResponse, SuccessResponse } from '../types.js' -import { generatePhoneNumber, generateZipCode, generateStreetAddress } from './generators.js' +import { getElement, generateRandomInt } from '../utils.js'; +import { ErrorResponse, SuccessResponse } from '../types.js'; +import { generatePhoneNumber, generateZipCode, generateStreetAddress } from './generators.js'; /** * @param {Record} action @@ -8,26 +8,28 @@ import { generatePhoneNumber, generateZipCode, generateStreetAddress } from './g * @param {Document | HTMLElement} root * @return {import('../types.js').ActionResponse} */ -export function fillForm (action, userData, root = document) { - const form = getElement(root, action.selector) - if (!form) return new ErrorResponse({ actionID: action.id, message: 'missing form' }) - if (!userData) return new ErrorResponse({ actionID: action.id, message: 'user data was absent' }) +export function fillForm(action, userData, root = document) { + const form = getElement(root, action.selector); + if (!form) return new ErrorResponse({ actionID: action.id, message: 'missing form' }); + if (!userData) return new ErrorResponse({ actionID: action.id, message: 'user data was absent' }); // ensure the element is in the current viewport - form.scrollIntoView?.() + form.scrollIntoView?.(); - const results = fillMany(form, action.elements, userData) + const results = fillMany(form, action.elements, userData); - const errors = results.filter(x => x.result === false).map(x => { - if ('error' in x) return x.error - return 'unknown error' - }) + const errors = results + .filter((x) => x.result === false) + .map((x) => { + if ('error' in x) return x.error; + return 'unknown error'; + }); if (errors.length > 0) { - return new ErrorResponse({ actionID: action.id, message: errors.join(', ') }) + return new ErrorResponse({ actionID: action.id, message: errors.join(', ') }); } - return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }) + return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null }); } /** @@ -37,60 +39,75 @@ export function fillForm (action, userData, root = document) { * @param {Record} data * @return {({result: true} | {result: false; error: string})[]} */ -export function fillMany (root, elements, data) { - const results = [] +export function fillMany(root, elements, data) { + const results = []; for (const element of elements) { - const inputElem = getElement(root, element.selector) + const inputElem = getElement(root, element.selector); if (!inputElem) { - results.push({ result: false, error: `element not found for selector: "${element.selector}"` }) - continue + results.push({ result: false, error: `element not found for selector: "${element.selector}"` }); + continue; } if (element.type === '$file_id$') { - results.push(setImageUpload(inputElem)) + results.push(setImageUpload(inputElem)); } else if (element.type === '$generated_phone_number$') { - results.push(setValueForInput(inputElem, generatePhoneNumber())) + results.push(setValueForInput(inputElem, generatePhoneNumber())); } else if (element.type === '$generated_zip_code$') { - results.push(setValueForInput(inputElem, generateZipCode())) + results.push(setValueForInput(inputElem, generateZipCode())); } else if (element.type === '$generated_random_number$') { if (!element.min || !element.max) { - results.push({ result: false, error: `element found with selector '${element.selector}', but missing min and/or max values` }) - continue + results.push({ + result: false, + error: `element found with selector '${element.selector}', but missing min and/or max values`, + }); + continue; } - const minInt = parseInt(element?.min) - const maxInt = parseInt(element?.max) + const minInt = parseInt(element?.min); + const maxInt = parseInt(element?.max); if (isNaN(minInt) || isNaN(maxInt)) { - results.push({ result: false, error: `element found with selector '${element.selector}', but min or max was not a number` }) - continue + results.push({ + result: false, + error: `element found with selector '${element.selector}', but min or max was not a number`, + }); + continue; } - results.push(setValueForInput(inputElem, generateRandomInt(parseInt(element.min), parseInt(element.max)).toString())) + results.push(setValueForInput(inputElem, generateRandomInt(parseInt(element.min), parseInt(element.max)).toString())); } else if (element.type === '$generated_street_address$') { - results.push(setValueForInput(inputElem, generateStreetAddress())) + results.push(setValueForInput(inputElem, generateStreetAddress())); - // This is a composite of existing (but separate) city and state fields + // This is a composite of existing (but separate) city and state fields } else if (element.type === 'cityState') { if (!Object.prototype.hasOwnProperty.call(data, 'city') || !Object.prototype.hasOwnProperty.call(data, 'state')) { - results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the keys 'city' and 'state'` }) - continue + results.push({ + result: false, + error: `element found with selector '${element.selector}', but data didn't contain the keys 'city' and 'state'`, + }); + continue; } - results.push(setValueForInput(inputElem, data.city + ', ' + data.state)) + results.push(setValueForInput(inputElem, data.city + ', ' + data.state)); } else { if (!Object.prototype.hasOwnProperty.call(data, element.type)) { - results.push({ result: false, error: `element found with selector '${element.selector}', but data didn't contain the key '${element.type}'` }) - continue + results.push({ + result: false, + error: `element found with selector '${element.selector}', but data didn't contain the key '${element.type}'`, + }); + continue; } if (!data[element.type]) { - results.push({ result: false, error: `data contained the key '${element.type}', but it wasn't something we can fill: ${data[element.type]}` }) - continue + results.push({ + result: false, + error: `data contained the key '${element.type}', but it wasn't something we can fill: ${data[element.type]}`, + }); + continue; } - results.push(setValueForInput(inputElem, data[element.type])) + results.push(setValueForInput(inputElem, data[element.type])); } } - return results + return results; } /** @@ -102,57 +119,57 @@ export function fillMany (root, elements, data) { * @param {string} val * @return {{result: true} | {result: false; error: string}} */ -function setValueForInput (el, val) { +function setValueForInput(el, val) { // Access the original setters // originally needed to bypass React's implementation on mobile - let target - if (el.tagName === 'INPUT') target = window.HTMLInputElement - if (el.tagName === 'SELECT') target = window.HTMLSelectElement + let target; + if (el.tagName === 'INPUT') target = window.HTMLInputElement; + if (el.tagName === 'SELECT') target = window.HTMLSelectElement; // Bail early if we cannot fill this element if (!target) { - return { result: false, error: `input type was not supported: ${el.tagName}` } + return { result: false, error: `input type was not supported: ${el.tagName}` }; } - const originalSet = Object.getOwnPropertyDescriptor(target.prototype, 'value')?.set + const originalSet = Object.getOwnPropertyDescriptor(target.prototype, 'value')?.set; // ensure it's a callable method if (!originalSet || typeof originalSet.call !== 'function') { - return { result: false, error: 'cannot access original value setter' } + return { result: false, error: 'cannot access original value setter' }; } try { // separate strategies for inputs vs selects if (el.tagName === 'INPUT') { // set the input value - el.dispatchEvent(new Event('keydown', { bubbles: true })) - originalSet.call(el, val) + el.dispatchEvent(new Event('keydown', { bubbles: true })); + originalSet.call(el, val); const events = [ new Event('input', { bubbles: true }), new Event('keyup', { bubbles: true }), - new Event('change', { bubbles: true }) - ] - events.forEach((ev) => el.dispatchEvent(ev)) - originalSet.call(el, val) - events.forEach((ev) => el.dispatchEvent(ev)) - el.blur() + new Event('change', { bubbles: true }), + ]; + events.forEach((ev) => el.dispatchEvent(ev)); + originalSet.call(el, val); + events.forEach((ev) => el.dispatchEvent(ev)); + el.blur(); } else if (el.tagName === 'SELECT') { // set the select value - originalSet.call(el, val) + originalSet.call(el, val); const events = [ new Event('mousedown', { bubbles: true }), new Event('mouseup', { bubbles: true }), new Event('click', { bubbles: true }), - new Event('change', { bubbles: true }) - ] - events.forEach((ev) => el.dispatchEvent(ev)) - events.forEach((ev) => el.dispatchEvent(ev)) - el.blur() + new Event('change', { bubbles: true }), + ]; + events.forEach((ev) => el.dispatchEvent(ev)); + events.forEach((ev) => el.dispatchEvent(ev)); + el.blur(); } - return { result: true } + return { result: true }; } catch (e) { - return { result: false, error: `setValueForInput exception: ${e}` } + return { result: false, error: `setValueForInput exception: ${e}` }; } } @@ -160,31 +177,32 @@ function setValueForInput (el, val) { * @param element * @return {{result: true}|{result: false, error: string}} */ -function setImageUpload (element) { - const base64PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/B8AAusB9VF9PmUAAAAASUVORK5CYII=' +function setImageUpload(element) { + const base64PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/B8AAusB9VF9PmUAAAAASUVORK5CYII='; try { // Convert the Base64 string to a Blob - const binaryString = window.atob(base64PNG) + const binaryString = window.atob(base64PNG); // Convert binary string to a Typed Array - const length = binaryString.length - const bytes = new Uint8Array(length) + const length = binaryString.length; + const bytes = new Uint8Array(length); for (let i = 0; i < length; i++) { - bytes[i] = binaryString.charCodeAt(i) + bytes[i] = binaryString.charCodeAt(i); } // Create the Blob from the Typed Array - const blob = new Blob([bytes], { type: 'image/png' }) + const blob = new Blob([bytes], { type: 'image/png' }); // Create a DataTransfer object and append the Blob - const dataTransfer = new DataTransfer() + const dataTransfer = new DataTransfer(); dataTransfer.items.add(new File([blob], 'id.png', { type: 'image/png' })); // Step 4: Assign the Blob to the Input Element - /** @type {any} */(element).files = dataTransfer.files - return { result: true } + /** @type {any} */ + element.files = dataTransfer.files; + return { result: true }; } catch (e) { // failed - return { result: false, error: e.toString() } + return { result: false, error: e.toString() }; } } diff --git a/injected/src/features/broker-protection/actions/generators.js b/injected/src/features/broker-protection/actions/generators.js index c83b9a513..f6624420b 100644 --- a/injected/src/features/broker-protection/actions/generators.js +++ b/injected/src/features/broker-protection/actions/generators.js @@ -1,31 +1,49 @@ -import { generateRandomInt } from '../utils.js' +import { generateRandomInt } from '../utils.js'; -export function generatePhoneNumber () { +export function generatePhoneNumber() { /** * 3 digits, 2-8, last two digits technically can't end in two 1s, but we'll ignore that requirement * Source: https://math.stackexchange.com/questions/920972/how-many-different-phone-numbers-are-possible-within-an-area-code/1115411#1115411 */ - const areaCode = generateRandomInt(200, 899).toString() + const areaCode = generateRandomInt(200, 899).toString(); // 555-0100 through 555-0199 are for fictional use (https://en.wikipedia.org/wiki/555_(telephone_number)#Fictional_usage) - const exchangeCode = '555' - const lineNumber = generateRandomInt(100, 199).toString().padStart(4, '0') + const exchangeCode = '555'; + const lineNumber = generateRandomInt(100, 199).toString().padStart(4, '0'); - return `${areaCode}${exchangeCode}${lineNumber}` + return `${areaCode}${exchangeCode}${lineNumber}`; } -export function generateZipCode () { - const zipCode = generateRandomInt(10000, 99999).toString() - return zipCode +export function generateZipCode() { + const zipCode = generateRandomInt(10000, 99999).toString(); + return zipCode; } -export function generateStreetAddress () { - const streetDigits = generateRandomInt(1, 5) - const streetNumber = generateRandomInt(2, streetDigits * 1000) - const streetNames = ['Main', 'Elm', 'Maple', 'Oak', 'Pine', 'Cedar', 'Hill', 'Lake', 'Sunset', 'Washington', 'Lincoln', 'Marshall', 'Spring', 'Ridge', 'Valley', 'Meadow', 'Forest'] - const streetName = streetNames[generateRandomInt(0, streetNames.length - 1)] - const suffixes = ['', 'St', 'Ave', 'Blvd', 'Rd', 'Ct', 'Dr', 'Ln', 'Pkwy', 'Pl', 'Ter', 'Way'] - const suffix = suffixes[generateRandomInt(0, suffixes.length - 1)] +export function generateStreetAddress() { + const streetDigits = generateRandomInt(1, 5); + const streetNumber = generateRandomInt(2, streetDigits * 1000); + const streetNames = [ + 'Main', + 'Elm', + 'Maple', + 'Oak', + 'Pine', + 'Cedar', + 'Hill', + 'Lake', + 'Sunset', + 'Washington', + 'Lincoln', + 'Marshall', + 'Spring', + 'Ridge', + 'Valley', + 'Meadow', + 'Forest', + ]; + const streetName = streetNames[generateRandomInt(0, streetNames.length - 1)]; + const suffixes = ['', 'St', 'Ave', 'Blvd', 'Rd', 'Ct', 'Dr', 'Ln', 'Pkwy', 'Pl', 'Ter', 'Way']; + const suffix = suffixes[generateRandomInt(0, suffixes.length - 1)]; - return `${streetNumber} ${streetName}${suffix ? ' ' + suffix : ''}` + return `${streetNumber} ${streetName}${suffix ? ' ' + suffix : ''}`; } diff --git a/injected/src/features/broker-protection/comparisons/address.js b/injected/src/features/broker-protection/comparisons/address.js index 12f251935..d5ac3debc 100644 --- a/injected/src/features/broker-protection/comparisons/address.js +++ b/injected/src/features/broker-protection/comparisons/address.js @@ -1,23 +1,25 @@ -import { states } from './constants.js' -import { matchingPair } from '../utils.js' +import { states } from './constants.js'; +import { matchingPair } from '../utils.js'; /** * @param {{city: string; state: string | null}[]} userAddresses * @param {{city: string; state: string | null}[]} foundAddresses * @return {boolean} */ -export function addressMatch (userAddresses, foundAddresses) { +export function addressMatch(userAddresses, foundAddresses) { return userAddresses.some((user) => { - return foundAddresses.some(found => { - return matchingPair(user.city, found.city) && matchingPair(user.state, found.state) - }) - }) + return foundAddresses.some((found) => { + return matchingPair(user.city, found.city) && matchingPair(user.state, found.state); + }); + }); } -export function getStateFromAbbreviation (stateAbbreviation) { - if (stateAbbreviation == null || stateAbbreviation.trim() === '') { return null } +export function getStateFromAbbreviation(stateAbbreviation) { + if (stateAbbreviation == null || stateAbbreviation.trim() === '') { + return null; + } - const state = stateAbbreviation.toUpperCase() + const state = stateAbbreviation.toUpperCase(); - return states[state] || null + return states[state] || null; } diff --git a/injected/src/features/broker-protection/comparisons/constants.js b/injected/src/features/broker-protection/comparisons/constants.js index b6b17b1bd..6d902222f 100644 --- a/injected/src/features/broker-protection/comparisons/constants.js +++ b/injected/src/features/broker-protection/comparisons/constants.js @@ -7,8 +7,8 @@ export const names = { * when hoisted by bundlers * @return {Record} */ - get nicknames () { - if (this._memo !== null) return this._memo + get nicknames() { + if (this._memo !== null) return this._memo; this._memo = { aaron: ['erin', 'ronnie', 'ron'], abbigail: ['nabby', 'abby', 'gail', 'abbe', 'abbi', 'abbey', 'abbie'], @@ -212,17 +212,7 @@ export const names = { carmellia: ['mellia'], carmelo: ['melo'], carmon: ['charm', 'cammie', 'carm'], - carol: [ - 'lynn', - 'carrie', - 'carolann', - 'cassie', - 'caroline', - 'carole', - 'carri', - 'kari', - 'kara' - ], + carol: ['lynn', 'carrie', 'carolann', 'cassie', 'caroline', 'carole', 'carri', 'kari', 'kara'], carolann: ['carol', 'carole'], caroline: ['lynn', 'carol', 'carrie', 'cassie', 'carole'], carolyn: ['lynn', 'carrie', 'cassie'], @@ -233,30 +223,8 @@ export const names = { cassandra: ['sandy', 'cassie', 'sandra'], cassidy: ['cassie', 'cass'], caswell: ['cass'], - catherine: [ - 'kathy', - 'katy', - 'lena', - 'kittie', - 'kit', - 'trina', - 'cathy', - 'kay', - 'cassie', - 'casey' - ], - cathleen: [ - 'kathy', - 'katy', - 'lena', - 'kittie', - 'kit', - 'trina', - 'cathy', - 'kay', - 'cassie', - 'casey' - ], + catherine: ['kathy', 'katy', 'lena', 'kittie', 'kit', 'trina', 'cathy', 'kay', 'cassie', 'casey'], + cathleen: ['kathy', 'katy', 'lena', 'kittie', 'kit', 'trina', 'cathy', 'kay', 'cassie', 'casey'], cathy: ['kathy', 'cathleen', 'catherine'], cecilia: ['cissy', 'celia'], cedric: ['ced', 'rick', 'ricky'], @@ -279,24 +247,8 @@ export const names = { christian: ['chris', 'kit'], christiana: ['kris', 'kristy', 'ann', 'tina', 'christy', 'chris', 'crissy'], christiano: ['chris'], - christina: [ - 'kris', - 'kristy', - 'tina', - 'christy', - 'chris', - 'crissy', - 'chrissy' - ], - christine: [ - 'kris', - 'kristy', - 'chrissy', - 'tina', - 'chris', - 'crissy', - 'christy' - ], + christina: ['kris', 'kristy', 'tina', 'christy', 'chris', 'crissy', 'chrissy'], + christine: ['kris', 'kristy', 'chrissy', 'tina', 'chris', 'crissy', 'christy'], christoph: ['chris'], christopher: ['chris', 'kit'], christy: ['crissy'], @@ -422,21 +374,7 @@ export const names = { elisa: ['lisa'], elisha: ['lish', 'eli'], eliza: ['elizabeth'], - elizabeth: [ - 'libby', - 'lisa', - 'lib', - 'lizzy', - 'lizzie', - 'eliza', - 'betsy', - 'liza', - 'betty', - 'bessie', - 'bess', - 'beth', - 'liz' - ], + elizabeth: ['libby', 'lisa', 'lib', 'lizzy', 'lizzie', 'eliza', 'betsy', 'liza', 'betty', 'bessie', 'bess', 'beth', 'liz'], ella: ['ellen', 'el'], ellen: ['nellie', 'nell', 'helen'], ellender: ['nellie', 'ellen', 'helen'], @@ -487,16 +425,7 @@ export const names = { ezideen: ['ez'], ezra: ['ez'], faith: ['fay'], - fallon: [ - 'falon', - 'fal', - 'fall', - 'fallie', - 'fally', - 'falcon', - 'lon', - 'lonnie' - ], + fallon: ['falon', 'fal', 'fall', 'fallie', 'fally', 'falcon', 'lon', 'lonnie'], felicia: ['fel', 'felix', 'feli'], felicity: ['flick', 'tick'], feltie: ['felty'], @@ -508,17 +437,7 @@ export const names = { florence: ['flossy', 'flora', 'flo'], floyd: ['lloyd'], fran: ['frannie'], - frances: [ - 'sis', - 'cissy', - 'frankie', - 'franniey', - 'fran', - 'francie', - 'frannie', - 'fanny', - 'franny' - ], + frances: ['sis', 'cissy', 'frankie', 'franniey', 'fran', 'francie', 'frannie', 'fanny', 'franny'], francie: ['francine'], francine: ['franniey', 'fran', 'frannie', 'francie', 'franny'], francis: ['fran', 'frankie', 'frank'], @@ -527,32 +446,14 @@ export const names = { franklind: ['fran', 'frank'], freda: ['frieda'], frederica: ['frederick', 'freddy', 'erika', 'erica', 'rickey'], - frederick: [ - 'freddie', - 'freddy', - 'fritz', - 'fred', - 'erick', - 'ricky', - 'derick', - 'rick' - ], + frederick: ['freddie', 'freddy', 'fritz', 'fred', 'erick', 'ricky', 'derick', 'rick'], fredericka: ['freddy', 'ricka', 'freda', 'frieda', 'ericka', 'rickey'], frieda: ['freddie', 'freddy', 'fred'], gabriel: ['gabe', 'gabby'], gabriella: ['ella', 'gabby'], gabrielle: ['ella', 'gabby'], gareth: ['gary', 'gare'], - garrett: [ - 'gare', - 'gary', - 'garry', - 'rhett', - 'garratt', - 'garret', - 'barrett', - 'jerry' - ], + garrett: ['gare', 'gary', 'garry', 'rhett', 'garratt', 'garret', 'barrett', 'jerry'], garrick: ['garri'], genevieve: ['jean', 'eve', 'jenny'], geoffrey: ['geoff', 'jeff'], @@ -591,16 +492,7 @@ export const names = { hayley: ['hailey', 'haylee'], heather: ['hetty'], helen: ['lena', 'ella', 'ellen', 'ellie'], - helena: [ - 'eileen', - 'lena', - 'nell', - 'nellie', - 'eleanor', - 'elaine', - 'ellen', - 'aileen' - ], + helena: ['eileen', 'lena', 'nell', 'nellie', 'eleanor', 'elaine', 'ellen', 'aileen'], helene: ['lena', 'ella', 'ellen', 'ellie'], heloise: ['lois', 'eloise', 'elouise'], henrietta: ['hank', 'etta', 'etty', 'retta', 'nettie', 'henny'], @@ -649,21 +541,7 @@ export const names = { jacqueline: ['jackie', 'jack', 'jacqui'], jahoda: ['hody', 'hodie', 'hoda'], jakob: ['jake'], - jalen: [ - 'jay', - 'jaye', - 'len', - 'lenny', - 'lennie', - 'jaylin', - 'alen', - 'al', - 'haylen', - 'jaelin', - 'jaelyn', - 'jailyn', - 'jaylyn' - ], + jalen: ['jay', 'jaye', 'len', 'lenny', 'lennie', 'jaylin', 'alen', 'al', 'haylen', 'jaelin', 'jaelyn', 'jailyn', 'jaylyn'], james: ['jimmy', 'jim', 'jamie', 'jimmie', 'jem'], jamey: ['james', 'jamie'], jamie: ['james'], @@ -709,49 +587,11 @@ export const names = { johannah: ['hannah', 'jody', 'joan', 'nonie', 'jo'], johannes: ['jonathan', 'john', 'johnny'], john: ['jon', 'johnny', 'jonny', 'jonnie', 'jack', 'jock', 'ian'], - johnathan: [ - 'johnathon', - 'jonathan', - 'jonathon', - 'jon', - 'jonny', - 'john', - 'johny', - 'jonnie', - 'nathan' - ], - johnathon: [ - 'johnathan', - 'jonathon', - 'jonathan', - 'jon', - 'jonny', - 'john', - 'johny', - 'jonnie' - ], + johnathan: ['johnathon', 'jonathan', 'jonathon', 'jon', 'jonny', 'john', 'johny', 'jonnie', 'nathan'], + johnathon: ['johnathan', 'jonathon', 'jonathan', 'jon', 'jonny', 'john', 'johny', 'jonnie'], jon: ['john', 'johnny', 'jonny', 'jonnie'], - jonathan: [ - 'johnathan', - 'johnathon', - 'jonathon', - 'jon', - 'jonny', - 'john', - 'johny', - 'jonnie', - 'nathan' - ], - jonathon: [ - 'johnathan', - 'johnathon', - 'jonathan', - 'jon', - 'jonny', - 'john', - 'johny', - 'jonnie' - ], + jonathan: ['johnathan', 'johnathon', 'jonathon', 'jon', 'jonny', 'john', 'johny', 'jonnie', 'nathan'], + jonathon: ['johnathan', 'johnathon', 'jonathan', 'jon', 'jonny', 'john', 'johny', 'jonnie'], joseph: ['jody', 'jos', 'joe', 'joey'], josephine: ['fina', 'jody', 'jo', 'josey', 'joey', 'josie'], josetta: ['jettie'], @@ -783,30 +623,8 @@ export const names = { kate: ['kay'], katelin: ['kay', 'kate', 'kaye'], katelyn: ['kay', 'kate', 'kaye'], - katherine: [ - 'kathy', - 'katy', - 'lena', - 'kittie', - 'kaye', - 'kit', - 'trina', - 'cathy', - 'kay', - 'kate', - 'cassie' - ], - kathleen: [ - 'kathy', - 'katy', - 'lena', - 'kittie', - 'kit', - 'trina', - 'cathy', - 'kay', - 'cassie' - ], + katherine: ['kathy', 'katy', 'lena', 'kittie', 'kaye', 'kit', 'trina', 'cathy', 'kay', 'kate', 'cassie'], + kathleen: ['kathy', 'katy', 'lena', 'kittie', 'kit', 'trina', 'cathy', 'kay', 'cassie'], kathryn: ['kathy', 'katie', 'kate'], katia: ['kate', 'katie'], katy: ['kathy', 'katie', 'kate'], @@ -918,17 +736,7 @@ export const names = { mackenzie: ['kenzy', 'mac', 'mack'], maddison: ['maddie', 'maddi'], maddy: ['madelyn', 'madeline', 'madge'], - madeline: [ - 'maggie', - 'lena', - 'magda', - 'maddy', - 'madge', - 'maddie', - 'maddi', - 'madie', - 'maud' - ], + madeline: ['maggie', 'lena', 'magda', 'maddy', 'madge', 'maddie', 'maddi', 'madie', 'maud'], madelyn: ['maddy', 'madie'], madie: ['madeline', 'madelyn'], madison: ['mattie', 'maddy'], @@ -963,22 +771,9 @@ export const names = { 'daisy', 'margery', 'gretta', - 'rita' - ], - margaretta: [ - 'maggie', - 'meg', - 'peg', - 'midge', - 'margie', - 'madge', - 'peggy', - 'marge', - 'daisy', - 'margery', - 'gretta', - 'rita' + 'rita', ], + margaretta: ['maggie', 'meg', 'peg', 'midge', 'margie', 'madge', 'peggy', 'marge', 'daisy', 'margery', 'gretta', 'rita'], margarita: [ 'maggie', 'meg', @@ -992,7 +787,7 @@ export const names = { 'daisy', 'peggie', 'rita', - 'margo' + 'margo', ], marge: ['margery', 'margaret', 'margaretta'], margie: ['marjorie'], @@ -1015,7 +810,7 @@ export const names = { 'marie', 'mamie', 'mary', - 'maria' + 'maria', ], marilyn: ['mary'], marion: ['mary'], @@ -1069,18 +864,7 @@ export const names = { micheal: ['mike', 'miky', 'mikey'], michelle: ['mickey', 'shelley', 'shely', 'chelle', 'shellie', 'shelly'], mick: ['micky'], - miguel: [ - 'miguell', - 'miguael', - 'miguaell', - 'miguail', - 'miguaill', - 'miguayl', - 'miguayll', - 'michael', - 'mike', - 'miggy' - ], + miguel: ['miguell', 'miguael', 'miguaell', 'miguail', 'miguaill', 'miguayl', 'miguayll', 'michael', 'mike', 'miggy'], mike: ['micky', 'mick', 'michael'], mildred: ['milly'], millicent: ['missy', 'milly'], @@ -1117,15 +901,7 @@ export const names = { newt: ['newton'], newton: ['newt'], nicholas: ['nick', 'claes', 'claas', 'nic', 'nicky', 'nico', 'nickie'], - nicholette: [ - 'nickey', - 'nikki', - 'cole', - 'nicki', - 'nicky', - 'nichole', - 'nicole' - ], + nicholette: ['nickey', 'nikki', 'cole', 'nicki', 'nicky', 'nichole', 'nicole'], nicodemus: ['nick', 'nic', 'nicky', 'nico', 'nickie'], nicole: ['nole', 'nikki', 'cole', 'nicki', 'nicky'], nikolas: ['nick', 'claes', 'nic', 'nicky', 'nico', 'nickie'], @@ -1216,30 +992,10 @@ export const names = { rhyna: ['rhynie'], ricardo: ['rick', 'ricky'], rich: ['dick', 'rick'], - richard: [ - 'dick', - 'dickon', - 'dickie', - 'dicky', - 'rick', - 'rich', - 'ricky', - 'richie' - ], + richard: ['dick', 'dickon', 'dickie', 'dicky', 'rick', 'rich', 'ricky', 'richie'], rick: ['ricky'], ricky: ['dick', 'rich'], - robert: [ - 'hob', - 'hobkin', - 'dob', - 'rob', - 'bobby', - 'dobbin', - 'bob', - 'bill', - 'billy', - 'robby' - ], + robert: ['hob', 'hobkin', 'dob', 'rob', 'bobby', 'dobbin', 'bob', 'bill', 'billy', 'robby'], roberta: ['robbie', 'bert', 'bobbie', 'birdie', 'bertie', 'roby', 'birtie'], roberto: ['rob'], roderick: ['rod', 'erick', 'rickie', 'roddy'], @@ -1323,17 +1079,7 @@ export const names = { stacie: ['stacy', 'stacey', 'staci'], stacy: ['staci'], stephan: ['steve'], - stephanie: [ - 'stephie', - 'annie', - 'steph', - 'stevie', - 'stephine', - 'stephany', - 'stephani', - 'steffi', - 'steffie' - ], + stephanie: ['stephie', 'annie', 'steph', 'stevie', 'stephine', 'stephany', 'stephani', 'steffi', 'steffie'], stephen: ['steve', 'steph'], steven: ['steve', 'steph', 'stevie'], stuart: ['stu'], @@ -1370,17 +1116,7 @@ export const names = { theodosia: ['theo', 'dosia', 'theodosius'], theophilus: ['ophi'], theotha: ['otha'], - theresa: [ - 'tessie', - 'thirza', - 'tessa', - 'terry', - 'tracy', - 'tess', - 'thursa', - 'traci', - 'tracie' - ], + theresa: ['tessie', 'thirza', 'tessa', 'terry', 'tracy', 'tess', 'thursa', 'traci', 'tracie'], thom: ['thomas', 'tommy', 'tom'], thomas: ['thom', 'tommy', 'tom'], thomasa: ['tamzine'], @@ -1409,30 +1145,11 @@ export const names = { vandalia: ['vannie'], vanessa: ['essa', 'vanna', 'nessa'], vernisee: ['nicey'], - veronica: [ - 'vonnie', - 'ron', - 'ronna', - 'ronie', - 'frony', - 'franky', - 'ronnie', - 'ronny' - ], + veronica: ['vonnie', 'ron', 'ronna', 'ronie', 'frony', 'franky', 'ronnie', 'ronny'], vic: ['vicki', 'vickie', 'vicky', 'victor'], vicki: ['vickie', 'vicky', 'victoria'], victor: ['vic'], - victoria: [ - 'torie', - 'vic', - 'vicki', - 'tory', - 'vicky', - 'tori', - 'torri', - 'torrie', - 'vickie' - ], + victoria: ['torie', 'vic', 'vicki', 'tory', 'vicky', 'tori', 'torri', 'torrie', 'vickie'], vijay: ['vij'], vincent: ['vic', 'vince', 'vinnie', 'vin', 'vinny'], vincenzo: ['vic', 'vinnie', 'vin', 'vinny', 'vince'], @@ -1480,11 +1197,11 @@ export const names = { zack: ['zach', 'zak'], zebedee: ['zeb'], zedediah: ['dyer', 'zed', 'diah'], - zephaniah: ['zeph'] - } - return this._memo - } -} + zephaniah: ['zeph'], + }; + return this._memo; + }, +}; export const states = { AL: 'Alabama', @@ -1537,5 +1254,5 @@ export const states = { WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', - WY: 'Wyoming' -} + WY: 'Wyoming', +}; diff --git a/injected/src/features/broker-protection/comparisons/is-same-age.js b/injected/src/features/broker-protection/comparisons/is-same-age.js index 849856fd2..17f1d11b8 100644 --- a/injected/src/features/broker-protection/comparisons/is-same-age.js +++ b/injected/src/features/broker-protection/comparisons/is-same-age.js @@ -3,19 +3,19 @@ * @param ageFound * @return {boolean} */ -export function isSameAge (userAge, ageFound) { +export function isSameAge(userAge, ageFound) { // variance allows for +/- 1 on the data broker and +/- 1 based on the only having a birth year - const ageVariance = 2 - userAge = parseInt(userAge) - ageFound = parseInt(ageFound) + const ageVariance = 2; + userAge = parseInt(userAge); + ageFound = parseInt(ageFound); if (isNaN(ageFound)) { - return false + return false; } if (Math.abs(userAge - ageFound) < ageVariance) { - return true + return true; } - return false + return false; } diff --git a/injected/src/features/broker-protection/comparisons/is-same-name.js b/injected/src/features/broker-protection/comparisons/is-same-name.js index fdaa0f755..6209cd80d 100644 --- a/injected/src/features/broker-protection/comparisons/is-same-name.js +++ b/injected/src/features/broker-protection/comparisons/is-same-name.js @@ -1,4 +1,4 @@ -import { names } from './constants.js' +import { names } from './constants.js'; /** * @param {string} fullNameExtracted @@ -8,83 +8,84 @@ import { names } from './constants.js' * @param {string | null} [userSuffix] * @return {boolean} */ -export function isSameName (fullNameExtracted, userFirstName, userMiddleName, userLastName, userSuffix) { +export function isSameName(fullNameExtracted, userFirstName, userMiddleName, userLastName, userSuffix) { // If there's no name on the website, then there's no way we can match it if (!fullNameExtracted) { - return false + return false; } // these fields should never be absent. If they are we cannot continue - if (!userFirstName || !userLastName) return false + if (!userFirstName || !userLastName) return false; - fullNameExtracted = fullNameExtracted.toLowerCase().trim().replace('.', '') - userFirstName = userFirstName.toLowerCase() - userMiddleName = userMiddleName ? userMiddleName.toLowerCase() : null - userLastName = userLastName.toLowerCase() - userSuffix = userSuffix ? userSuffix.toLowerCase() : null + fullNameExtracted = fullNameExtracted.toLowerCase().trim().replace('.', ''); + userFirstName = userFirstName.toLowerCase(); + userMiddleName = userMiddleName ? userMiddleName.toLowerCase() : null; + userLastName = userLastName.toLowerCase(); + userSuffix = userSuffix ? userSuffix.toLowerCase() : null; // Get a list of the user's name and nicknames / full names - const names = getNames(userFirstName) + const names = getNames(userFirstName); for (const firstName of names) { - // Let's check if the name matches right off the bat - const nameCombo1 = `${firstName} ${userLastName}` + // Let's check if the name matches right off the bat + const nameCombo1 = `${firstName} ${userLastName}`; if (fullNameExtracted === nameCombo1) { - return true + return true; } // If the user didn't supply a middle name, then try to match names extracted names that // might include a middle name. if (!userMiddleName) { - const combinedLength = firstName.length + userLastName.length - const matchesFirstAndLast = fullNameExtracted.startsWith(firstName) && + const combinedLength = firstName.length + userLastName.length; + const matchesFirstAndLast = + fullNameExtracted.startsWith(firstName) && fullNameExtracted.endsWith(userLastName) && - fullNameExtracted.length > combinedLength + fullNameExtracted.length > combinedLength; if (matchesFirstAndLast) { - return true + return true; } } // If there's a suffix, check that too if (userSuffix) { - const nameCombo1WithSuffix = `${firstName} ${userLastName} ${userSuffix}` + const nameCombo1WithSuffix = `${firstName} ${userLastName} ${userSuffix}`; if (fullNameExtracted === nameCombo1WithSuffix) { - return true + return true; } } // If the user has a name with a hyphen, we should split it on the hyphen // Note: They may have a last name or first name with a hyphen if (userLastName && userLastName.includes('-')) { - const userLastNameOption2 = userLastName.split('-').join(' ') - const userLastNameOption3 = userLastName.split('-').join('') - const userLastNameOption4 = userLastName.split('-')[0] + const userLastNameOption2 = userLastName.split('-').join(' '); + const userLastNameOption3 = userLastName.split('-').join(''); + const userLastNameOption4 = userLastName.split('-')[0]; const comparisons = [ `${firstName} ${userLastNameOption2}`, `${firstName} ${userLastNameOption3}`, - `${firstName} ${userLastNameOption4}` - ] + `${firstName} ${userLastNameOption4}`, + ]; if (comparisons.includes(fullNameExtracted)) { - return true + return true; } } // Treat first name with the same logic as the last name if (userFirstName && userFirstName.includes('-')) { - const userFirstNameOption2 = userFirstName.split('-').join(' ') - const userFirstNameOption3 = userFirstName.split('-').join('') - const userFirstNameOption4 = userFirstName.split('-')[0] + const userFirstNameOption2 = userFirstName.split('-').join(' '); + const userFirstNameOption3 = userFirstName.split('-').join(''); + const userFirstNameOption4 = userFirstName.split('-')[0]; const comparisons = [ `${userFirstNameOption2} ${userLastName}`, `${userFirstNameOption3} ${userLastName}`, - `${userFirstNameOption4} ${userLastName}` - ] + `${userFirstNameOption4} ${userLastName}`, + ]; if (comparisons.includes(fullNameExtracted)) { - return true + return true; } } @@ -97,37 +98,37 @@ export function isSameName (fullNameExtracted, userFirstName, userMiddleName, us `${firstName} ${userMiddleName[0]} ${userLastName}`, `${firstName} ${userMiddleName[0]} ${userLastName} ${userSuffix}`, `${firstName} ${userMiddleName}${userLastName}`, - `${firstName} ${userMiddleName}${userLastName} ${userSuffix}` - ] + `${firstName} ${userMiddleName}${userLastName} ${userSuffix}`, + ]; if (comparisons.includes(fullNameExtracted)) { - return true + return true; } // If it's a hyphenated last name, we have more to try if (userLastName && userLastName.includes('-')) { - const userLastNameOption2 = userLastName.split('-').join(' ') - const userLastNameOption3 = userLastName.split('-').join('') - const userLastNameOption4 = userLastName.split('-')[0] + const userLastNameOption2 = userLastName.split('-').join(' '); + const userLastNameOption3 = userLastName.split('-').join(''); + const userLastNameOption4 = userLastName.split('-')[0]; const comparisons = [ `${firstName} ${userMiddleName} ${userLastNameOption2}`, `${firstName} ${userMiddleName} ${userLastNameOption4}`, `${firstName} ${userMiddleName[0]} ${userLastNameOption2}`, `${firstName} ${userMiddleName[0]} ${userLastNameOption3}`, - `${firstName} ${userMiddleName[0]} ${userLastNameOption4}` - ] + `${firstName} ${userMiddleName[0]} ${userLastNameOption4}`, + ]; if (comparisons.includes(fullNameExtracted)) { - return true + return true; } } // If it's a hyphenated name, we have more to try if (userFirstName && userFirstName.includes('-')) { - const userFirstNameOption2 = userFirstName.split('-').join(' ') - const userFirstNameOption3 = userFirstName.split('-').join('') - const userFirstNameOption4 = userFirstName.split('-')[0] + const userFirstNameOption2 = userFirstName.split('-').join(' '); + const userFirstNameOption3 = userFirstName.split('-').join(''); + const userFirstNameOption4 = userFirstName.split('-')[0]; const comparisons = [ `${userFirstNameOption2} ${userMiddleName} ${userLastName}`, @@ -135,17 +136,17 @@ export function isSameName (fullNameExtracted, userFirstName, userMiddleName, us `${userFirstNameOption4} ${userMiddleName} ${userLastName}`, `${userFirstNameOption2} ${userMiddleName[0]} ${userLastName}`, `${userFirstNameOption3} ${userMiddleName[0]} ${userLastName}`, - `${userFirstNameOption4} ${userMiddleName[0]} ${userLastName}` - ] + `${userFirstNameOption4} ${userMiddleName[0]} ${userLastName}`, + ]; if (comparisons.includes(fullNameExtracted)) { - return true + return true; } } } } - return false + return false; } /** @@ -154,13 +155,15 @@ export function isSameName (fullNameExtracted, userFirstName, userMiddleName, us * @param {string | null} name * @return {Set} */ -export function getNames (name) { - if (!noneEmptyString(name)) { return new Set() } +export function getNames(name) { + if (!noneEmptyString(name)) { + return new Set(); + } - name = name.toLowerCase() - const nicknames = names.nicknames + name = name.toLowerCase(); + const nicknames = names.nicknames; - return new Set([name, ...getNicknames(name, nicknames), ...getFullNames(name, nicknames)]) + return new Set([name, ...getNicknames(name, nicknames), ...getFullNames(name, nicknames)]); } /** @@ -170,18 +173,20 @@ export function getNames (name) { * @param {Record} nicknames * @return {Set} */ -export function getNicknames (name, nicknames) { - const emptySet = new Set() +export function getNicknames(name, nicknames) { + const emptySet = new Set(); - if (!noneEmptyString(name)) { return emptySet } + if (!noneEmptyString(name)) { + return emptySet; + } - name = name.toLowerCase() + name = name.toLowerCase(); if (Object.prototype.hasOwnProperty.call(nicknames, name)) { - return new Set(nicknames[name]) + return new Set(nicknames[name]); } - return emptySet + return emptySet; } /** @@ -191,20 +196,22 @@ export function getNicknames (name, nicknames) { * @param {Record} nicknames * @return {Set} */ -export function getFullNames (name, nicknames) { - const fullNames = new Set() +export function getFullNames(name, nicknames) { + const fullNames = new Set(); - if (!noneEmptyString(name)) { return fullNames } + if (!noneEmptyString(name)) { + return fullNames; + } - name = name.toLowerCase() + name = name.toLowerCase(); for (const fullName of Object.keys(nicknames)) { if (nicknames[fullName].includes(name)) { - fullNames.add(fullName) + fullNames.add(fullName); } } - return fullNames + return fullNames; } /** @@ -212,7 +219,7 @@ export function getFullNames (name, nicknames) { * @param {any} [input] * @return {input is string} */ -function noneEmptyString (input) { - if (typeof input !== 'string') return false - return input.trim().length > 0 +function noneEmptyString(input) { + if (typeof input !== 'string') return false; + return input.trim().length > 0; } diff --git a/injected/src/features/broker-protection/execute.js b/injected/src/features/broker-protection/execute.js index 46e63a70b..8cd5b1014 100644 --- a/injected/src/features/broker-protection/execute.js +++ b/injected/src/features/broker-protection/execute.js @@ -1,10 +1,10 @@ -import { buildUrl } from './actions/build-url.js' -import { extract } from './actions/extract.js' -import { fillForm } from './actions/fill-form.js' -import { getCaptchaInfo, solveCaptcha } from './actions/captcha.js' -import { click } from './actions/click.js' -import { expectation } from './actions/expectation.js' -import { ErrorResponse } from './types.js' +import { buildUrl } from './actions/build-url.js'; +import { extract } from './actions/extract.js'; +import { fillForm } from './actions/fill-form.js'; +import { getCaptchaInfo, solveCaptcha } from './actions/captcha.js'; +import { click } from './actions/click.js'; +import { expectation } from './actions/expectation.js'; +import { ErrorResponse } from './types.js'; /** * @param {object} action @@ -15,36 +15,36 @@ import { ErrorResponse } from './types.js' * @param {Document} [root] - optional root element * @return {Promise} */ -export async function execute (action, inputData, root = document) { +export async function execute(action, inputData, root = document) { try { switch (action.actionType) { - case 'navigate': - return buildUrl(action, data(action, inputData, 'userProfile')) - case 'extract': - return await extract(action, data(action, inputData, 'userProfile'), root) - case 'click': - return click(action, data(action, inputData, 'userProfile'), root) - case 'expectation': - return await expectation(action, data(action, inputData, 'userProfile'), root) - case 'fillForm': - return fillForm(action, data(action, inputData, 'extractedProfile'), root) - case 'getCaptchaInfo': - return getCaptchaInfo(action, root) - case 'solveCaptcha': - return solveCaptcha(action, data(action, inputData, 'token'), root) - default: { - return new ErrorResponse({ - actionID: action.id, - message: `unimplemented actionType: ${action.actionType}` - }) - } + case 'navigate': + return buildUrl(action, data(action, inputData, 'userProfile')); + case 'extract': + return await extract(action, data(action, inputData, 'userProfile'), root); + case 'click': + return click(action, data(action, inputData, 'userProfile'), root); + case 'expectation': + return await expectation(action, data(action, inputData, 'userProfile'), root); + case 'fillForm': + return fillForm(action, data(action, inputData, 'extractedProfile'), root); + case 'getCaptchaInfo': + return getCaptchaInfo(action, root); + case 'solveCaptcha': + return solveCaptcha(action, data(action, inputData, 'token'), root); + default: { + return new ErrorResponse({ + actionID: action.id, + message: `unimplemented actionType: ${action.actionType}`, + }); + } } } catch (e) { - console.log('unhandled exception: ', e) + console.log('unhandled exception: ', e); return new ErrorResponse({ actionID: action.id, - message: `unhandled exception: ${e.message}` - }) + message: `unhandled exception: ${e.message}`, + }); } } @@ -53,11 +53,11 @@ export async function execute (action, inputData, root = document) { * @param {Record} data * @param {string} defaultSource */ -function data (action, data, defaultSource) { - if (!data) return null - const source = action.dataSource || defaultSource +function data(action, data, defaultSource) { + if (!data) return null; + const source = action.dataSource || defaultSource; if (Object.prototype.hasOwnProperty.call(data, source)) { - return data[source] + return data[source]; } - return null + return null; } diff --git a/injected/src/features/broker-protection/extractors/address.js b/injected/src/features/broker-protection/extractors/address.js index b168e7f2b..5ba139014 100644 --- a/injected/src/features/broker-protection/extractors/address.js +++ b/injected/src/features/broker-protection/extractors/address.js @@ -1,8 +1,8 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Extractor } from '../types.js' -import { stringToList } from '../actions/extract.js' -import parseAddress from 'parse-address' -import { states } from '../comparisons/constants.js' +import { Extractor } from '../types.js'; +import { stringToList } from '../actions/extract.js'; +import parseAddress from 'parse-address'; +import { states } from '../comparisons/constants.js'; /** * @implements {Extractor<{city:string; state: string|null}[]>} @@ -12,9 +12,9 @@ export class CityStateExtractor { * @param {string[]} strs * @param {import('../actions/extract.js').ExtractorParams} extractorParams */ - extract (strs, extractorParams) { - const cityStateList = strs.map(str => stringToList(str, extractorParams.separator)).flat() - return getCityStateCombos(cityStateList) + extract(strs, extractorParams) { + const cityStateList = strs.map((str) => stringToList(str, extractorParams.separator)).flat(); + return getCityStateCombos(cityStateList); } } @@ -26,17 +26,19 @@ export class AddressFullExtractor { * @param {string[]} strs * @param {import('../actions/extract.js').ExtractorParams} extractorParams */ - extract (strs, extractorParams) { - return strs - .map((str) => str.replace('\n', ' ')) - .map((str) => stringToList(str, extractorParams.separator)) - .flat() - .map((str) => parseAddress.parseLocation(str) || {}) - // at least 'city' is required. - .filter((parsed) => Boolean(parsed?.city)) - .map((addr) => { - return { city: addr.city, state: addr.state || null } - }) + extract(strs, extractorParams) { + return ( + strs + .map((str) => str.replace('\n', ' ')) + .map((str) => stringToList(str, extractorParams.separator)) + .flat() + .map((str) => parseAddress.parseLocation(str) || {}) + // at least 'city' is required. + .filter((parsed) => Boolean(parsed?.city)) + .map((addr) => { + return { city: addr.city, state: addr.state || null }; + }) + ); } } @@ -44,30 +46,32 @@ export class AddressFullExtractor { * @param {string[]} inputList * @return {{ city: string, state: string|null }[] } */ -function getCityStateCombos (inputList) { - const output = [] +function getCityStateCombos(inputList) { + const output = []; for (let item of inputList) { - let words + let words; // Strip out the zip code since we're only interested in city/state here. - item = item.replace(/,?\s*\d{5}(-\d{4})?/, '') + item = item.replace(/,?\s*\d{5}(-\d{4})?/, ''); if (item.includes(',')) { - words = item.split(',').map(item => item.trim()) + words = item.split(',').map((item) => item.trim()); } else { - words = item.split(' ').map(item => item.trim()) + words = item.split(' ').map((item) => item.trim()); } // we are removing this partial city/state combos at the end (i.e. Chi...) - if (words.length === 1) { continue } + if (words.length === 1) { + continue; + } - const state = words.pop() - const city = words.join(' ') + const state = words.pop(); + const city = words.join(' '); // exclude invalid states if (state && !Object.keys(states).includes(state.toUpperCase())) { - continue + continue; } - output.push({ city, state: state || null }) + output.push({ city, state: state || null }); } - return output + return output; } diff --git a/injected/src/features/broker-protection/extractors/age.js b/injected/src/features/broker-protection/extractors/age.js index 4a098747c..acc1d1061 100644 --- a/injected/src/features/broker-protection/extractors/age.js +++ b/injected/src/features/broker-protection/extractors/age.js @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Extractor } from '../types.js' +import { Extractor } from '../types.js'; /** * @implements {Extractor} @@ -9,9 +9,9 @@ export class AgeExtractor { * @param {string[]} strs * @param {import('../actions/extract.js').ExtractorParams} _extractorParams */ - - extract (strs, _extractorParams) { - if (!strs[0]) return null - return strs[0].match(/\d+/)?.[0] ?? null + + extract(strs, _extractorParams) { + if (!strs[0]) return null; + return strs[0].match(/\d+/)?.[0] ?? null; } } diff --git a/injected/src/features/broker-protection/extractors/name.js b/injected/src/features/broker-protection/extractors/name.js index 36470e88d..3945dae02 100644 --- a/injected/src/features/broker-protection/extractors/name.js +++ b/injected/src/features/broker-protection/extractors/name.js @@ -1,6 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Extractor } from '../types.js' -import { stringToList } from '../actions/extract.js' +import { Extractor } from '../types.js'; +import { stringToList } from '../actions/extract.js'; /** * @implements {Extractor} @@ -10,10 +10,10 @@ export class NameExtractor { * @param {string[]} strs * @param {import('../actions/extract.js').ExtractorParams} _extractorParams */ - - extract (strs, _extractorParams) { - if (!strs[0]) return null - return strs[0].replace(/\n/g, ' ').trim() + + extract(strs, _extractorParams) { + if (!strs[0]) return null; + return strs[0].replace(/\n/g, ' ').trim(); } } @@ -26,7 +26,7 @@ export class AlternativeNamesExtractor { * @param {import('../actions/extract.js').ExtractorParams} extractorParams * @returns {string[]} */ - extract (strs, extractorParams) { - return strs.map(x => stringToList(x, extractorParams.separator)).flat() + extract(strs, extractorParams) { + return strs.map((x) => stringToList(x, extractorParams.separator)).flat(); } } diff --git a/injected/src/features/broker-protection/extractors/phone.js b/injected/src/features/broker-protection/extractors/phone.js index a1883d5de..19546c8e3 100644 --- a/injected/src/features/broker-protection/extractors/phone.js +++ b/injected/src/features/broker-protection/extractors/phone.js @@ -1,6 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Extractor } from '../types.js' -import { stringToList } from '../actions/extract.js' +import { Extractor } from '../types.js'; +import { stringToList } from '../actions/extract.js'; /** * @implements {Extractor} @@ -10,9 +10,10 @@ export class PhoneExtractor { * @param {string[]} strs * @param {import('../actions/extract.js').ExtractorParams} extractorParams */ - extract (strs, extractorParams) { - return strs.map(str => stringToList(str, extractorParams.separator)) + extract(strs, extractorParams) { + return strs + .map((str) => stringToList(str, extractorParams.separator)) .flat() - .map(str => str.replace(/\D/g, '')) + .map((str) => str.replace(/\D/g, '')); } } diff --git a/injected/src/features/broker-protection/extractors/profile-url.js b/injected/src/features/broker-protection/extractors/profile-url.js index 3140bdb88..6fb8fbde5 100644 --- a/injected/src/features/broker-protection/extractors/profile-url.js +++ b/injected/src/features/broker-protection/extractors/profile-url.js @@ -1,6 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { AsyncProfileTransform, Extractor } from '../types.js' -import { hashObject } from '../utils.js' +import { AsyncProfileTransform, Extractor } from '../types.js'; +import { hashObject } from '../utils.js'; /** * @implements {Extractor<{profileUrl: string; identifier: string} | null>} @@ -10,20 +10,20 @@ export class ProfileUrlExtractor { * @param {string[]} strs * @param {import('../actions/extract.js').ExtractorParams} extractorParams */ - extract (strs, extractorParams) { - if (strs.length === 0) return null + extract(strs, extractorParams) { + if (strs.length === 0) return null; const profile = { profileUrl: strs[0], - identifier: strs[0] - } + identifier: strs[0], + }; if (!extractorParams.identifierType || !extractorParams.identifier) { - return profile + return profile; } - const profileUrl = strs[0] - profile.identifier = this.getIdFromProfileUrl(profileUrl, extractorParams.identifierType, extractorParams.identifier) - return profile + const profileUrl = strs[0]; + profile.identifier = this.getIdFromProfileUrl(profileUrl, extractorParams.identifierType, extractorParams.identifier); + return profile; } /** @@ -33,18 +33,18 @@ export class ProfileUrlExtractor { * @param {string} identifier * @return {string} */ - getIdFromProfileUrl (profileUrl, identifierType, identifier) { - const parsedUrl = new URL(profileUrl) - const urlParams = parsedUrl.searchParams + getIdFromProfileUrl(profileUrl, identifierType, identifier) { + const parsedUrl = new URL(profileUrl); + const urlParams = parsedUrl.searchParams; // Attempt to parse out an id from the search parameters if (identifierType === 'param' && urlParams.has(identifier)) { - const profileId = urlParams.get(identifier) - return profileId || profileUrl + const profileId = urlParams.get(identifier); + return profileId || profileUrl; } - return profileUrl - }; + return profileUrl; + } } /** @@ -59,14 +59,14 @@ export class ProfileHashTransformer { * @param {Record } params * @return {Promise>} */ - async transform (profile, params) { + async transform(profile, params) { if (params?.profileUrl?.identifierType !== 'hash') { - return profile + return profile; } return { ...profile, - identifier: await hashObject(profile) - } + identifier: await hashObject(profile), + }; } } diff --git a/injected/src/features/broker-protection/extractors/relatives.js b/injected/src/features/broker-protection/extractors/relatives.js index 3053b4d01..d9c757adb 100644 --- a/injected/src/features/broker-protection/extractors/relatives.js +++ b/injected/src/features/broker-protection/extractors/relatives.js @@ -1,6 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Extractor } from '../types.js' -import { stringToList } from '../actions/extract.js' +import { Extractor } from '../types.js'; +import { stringToList } from '../actions/extract.js'; /** * @implements {Extractor} @@ -10,10 +10,14 @@ export class RelativesExtractor { * @param {string[]} strs * @param {import('../actions/extract.js').ExtractorParams} extractorParams */ - extract (strs, extractorParams) { - return strs.map(x => stringToList(x, extractorParams.separator)).flat() - // for relatives, remove anything following a comma (usually 'age') - // eg: 'John Smith, 39' -> 'John Smith' - .map(x => x.split(',')[0]) + extract(strs, extractorParams) { + return ( + strs + .map((x) => stringToList(x, extractorParams.separator)) + .flat() + // for relatives, remove anything following a comma (usually 'age') + // eg: 'John Smith, 39' -> 'John Smith' + .map((x) => x.split(',')[0]) + ); } } diff --git a/injected/src/features/broker-protection/types.js b/injected/src/features/broker-protection/types.js index 193de0864..cef09cb70 100644 --- a/injected/src/features/broker-protection/types.js +++ b/injected/src/features/broker-protection/types.js @@ -9,12 +9,12 @@ */ export class ErrorResponse { /** - * @param {object} params - * @param {string} params.actionID - * @param {string} params.message - */ - constructor (params) { - this.error = params + * @param {object} params + * @param {string} params.actionID + * @param {string} params.message + */ + constructor(params) { + this.error = params; } } @@ -23,14 +23,14 @@ export class ErrorResponse { */ export class SuccessResponse { /** - * @param {object} params - * @param {string} params.actionID - * @param {string} params.actionType - * @param {any} params.response - * @param {Record} [params.meta] - optional meta data - */ - constructor (params) { - this.success = params + * @param {object} params + * @param {string} params.actionID + * @param {string} params.actionType + * @param {any} params.response + * @param {Record} [params.meta] - optional meta data + */ + constructor(params) { + this.success = params; } } @@ -47,25 +47,25 @@ export class ProfileResult { * @param {HTMLElement} [params.element] - the parent element that was matched. Not present in JSON * @param {Record} params.scrapedData */ - constructor (params) { - this.scrapedData = params.scrapedData - this.result = params.result - this.score = params.score - this.element = params.element - this.matchedFields = params.matchedFields + constructor(params) { + this.scrapedData = params.scrapedData; + this.result = params.result; + this.score = params.score; + this.element = params.element; + this.matchedFields = params.matchedFields; } /** * Convert this structure into a format that can be sent between JS contexts/native * @return {{result: boolean, score: number, matchedFields: string[], scrapedData: Record}} */ - asData () { + asData() { return { scrapedData: this.scrapedData, result: this.result, score: this.score, - matchedFields: this.matchedFields - } + matchedFields: this.matchedFields, + }; } } @@ -79,9 +79,9 @@ export class Extractor { * @param {import("./actions/extract").ExtractorParams} extractorParams * @return {JsonValue} */ - - extract (noneEmptyStringArray, extractorParams) { - throw new Error('must implement extract') + + extract(noneEmptyStringArray, extractorParams) { + throw new Error('must implement extract'); } } @@ -94,8 +94,8 @@ export class AsyncProfileTransform { * @param {Record} profileParams - the original action params from `action.profile` * @return {Promise>} */ - - transform (profile, profileParams) { - throw new Error('must implement extract') + + transform(profile, profileParams) { + throw new Error('must implement extract'); } } diff --git a/injected/src/features/broker-protection/utils.js b/injected/src/features/broker-protection/utils.js index bd835b7a6..c29914525 100644 --- a/injected/src/features/broker-protection/utils.js +++ b/injected/src/features/broker-protection/utils.js @@ -5,12 +5,12 @@ * @param {string} selector * @return {HTMLElement | null} */ -export function getElement (doc = document, selector) { +export function getElement(doc = document, selector) { if (isXpath(selector)) { - return safeQuerySelectorXPath(doc, selector) + return safeQuerySelectorXPath(doc, selector); } - return safeQuerySelector(doc, selector) + return safeQuerySelector(doc, selector); } /** @@ -20,12 +20,12 @@ export function getElement (doc = document, selector) { * @param {string} selector * @return {HTMLElement[] | null} */ -export function getElements (doc = document, selector) { +export function getElements(doc = document, selector) { if (isXpath(selector)) { - return safeQuerySelectorAllXpath(doc, selector) + return safeQuerySelectorAllXpath(doc, selector); } - return safeQuerySelectorAll(doc, selector) + return safeQuerySelectorAll(doc, selector); } /** @@ -34,16 +34,16 @@ export function getElements (doc = document, selector) { * @param {HTMLElement} element * @param {string} selector */ -export function getElementMatches (element, selector) { +export function getElementMatches(element, selector) { try { if (isXpath(selector)) { - return matchesXPath(element, selector) ? element : null + return matchesXPath(element, selector) ? element : null; } else { - return element.matches(selector) ? element : null + return element.matches(selector) ? element : null; } } catch (e) { - console.error('getElementMatches threw: ', e) - return null + console.error('getElementMatches threw: ', e); + return null; } } @@ -53,29 +53,23 @@ export function getElementMatches (element, selector) { * @param {string} selector * @return {boolean} */ -function matchesXPath (element, selector) { - const xpathResult = document.evaluate( - selector, - element, - null, - XPathResult.BOOLEAN_TYPE, - null - ) - - return xpathResult.booleanValue +function matchesXPath(element, selector) { + const xpathResult = document.evaluate(selector, element, null, XPathResult.BOOLEAN_TYPE, null); + + return xpathResult.booleanValue; } /** * @param {unknown} selector * @returns {boolean} */ -function isXpath (selector) { - if (!(typeof selector === 'string')) return false +function isXpath(selector) { + if (!(typeof selector === 'string')) return false; // see: https://www.w3.org/TR/xpath20/ // "When the context item is a node, it can also be referred to as the context node. The context item is returned by an expression consisting of a single dot" - if (selector === '.') return true - return selector.startsWith('//') || selector.startsWith('./') || selector.startsWith('(') + if (selector === '.') return true; + return selector.startsWith('//') || selector.startsWith('./') || selector.startsWith('('); } /** @@ -83,14 +77,14 @@ function isXpath (selector) { * @param selector * @returns {HTMLElement[] | null} */ -function safeQuerySelectorAll (element, selector) { +function safeQuerySelectorAll(element, selector) { try { if (element && 'querySelectorAll' in element) { - return Array.from(element?.querySelectorAll?.(selector)) + return Array.from(element?.querySelectorAll?.(selector)); } - return null + return null; } catch (e) { - return null + return null; } } /** @@ -98,14 +92,14 @@ function safeQuerySelectorAll (element, selector) { * @param selector * @returns {HTMLElement | null} */ -function safeQuerySelector (element, selector) { +function safeQuerySelector(element, selector) { try { if (element && 'querySelector' in element) { - return element?.querySelector?.(selector) + return element?.querySelector?.(selector); } - return null + return null; } catch (e) { - return null + return null; } } @@ -114,17 +108,17 @@ function safeQuerySelector (element, selector) { * @param selector * @returns {HTMLElement | null} */ -function safeQuerySelectorXPath (element, selector) { +function safeQuerySelectorXPath(element, selector) { try { - const match = document.evaluate(selector, element, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null) - const single = match?.singleNodeValue + const match = document.evaluate(selector, element, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); + const single = match?.singleNodeValue; if (single) { - return /** @type {HTMLElement} */(single) + return /** @type {HTMLElement} */ (single); } - return null + return null; } catch (e) { - console.log('safeQuerySelectorXPath threw', e) - return null + console.log('safeQuerySelectorXPath threw', e); + return null; } } @@ -133,23 +127,23 @@ function safeQuerySelectorXPath (element, selector) { * @param selector * @returns {HTMLElement[] | null} */ -function safeQuerySelectorAllXpath (element, selector) { +function safeQuerySelectorAllXpath(element, selector) { try { // gets all elements matching the xpath query - const xpathResult = document.evaluate(selector, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null) + const xpathResult = document.evaluate(selector, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); if (xpathResult) { /** @type {HTMLElement[]} */ - const matchedNodes = [] + const matchedNodes = []; for (let i = 0; i < xpathResult.snapshotLength; i++) { - const item = xpathResult.snapshotItem(i) - if (item) matchedNodes.push(/** @type {HTMLElement} */(item)) + const item = xpathResult.snapshotItem(i); + if (item) matchedNodes.push(/** @type {HTMLElement} */ (item)); } - return /** @type {HTMLElement[]} */(matchedNodes) + return /** @type {HTMLElement[]} */ (matchedNodes); } - return null + return null; } catch (e) { - console.log('safeQuerySelectorAllXpath threw', e) - return null + console.log('safeQuerySelectorAllXpath threw', e); + return null; } } @@ -158,8 +152,8 @@ function safeQuerySelectorAllXpath (element, selector) { * @param {number} max * @returns {number} */ -export function generateRandomInt (min, max) { - return Math.floor(Math.random() * (max - min + 1) + min) +export function generateRandomInt(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); } /** @@ -170,27 +164,27 @@ export function generateRandomInt (min, max) { * @param {NonNullable[]} prev * @return {NonNullable[]} - The cleaned array. */ -export function cleanArray (input, prev = []) { +export function cleanArray(input, prev = []) { if (!Array.isArray(input)) { - if (input === null) return prev - if (input === undefined) return prev + if (input === null) return prev; + if (input === undefined) return prev; // special case for empty strings if (typeof input === 'string') { - const trimmed = input.trim() + const trimmed = input.trim(); if (trimmed.length > 0) { - prev.push(/** @type {NonNullable} */(trimmed)) + prev.push(/** @type {NonNullable} */ (trimmed)); } } else { - prev.push(input) + prev.push(input); } - return prev + return prev; } for (const item of input) { - prev.push(...cleanArray(item)) + prev.push(...cleanArray(item)); } - return prev + return prev; } /** @@ -199,9 +193,9 @@ export function cleanArray (input, prev = []) { * @param {any} [input] - The input to be checked. * @return {boolean} - True if the input is a non-empty string, false otherwise. */ -export function nonEmptyString (input) { - if (typeof input !== 'string') return false - return input.trim().length > 0 +export function nonEmptyString(input) { + if (typeof input !== 'string') return false; + return input.trim().length > 0; } /** @@ -211,10 +205,10 @@ export function nonEmptyString (input) { * @param {any} b - The second string to compare. * @return {boolean} - Returns true if the strings are a matching pair, false otherwise. */ -export function matchingPair (a, b) { - if (!nonEmptyString(a)) return false - if (!nonEmptyString(b)) return false - return a.toLowerCase().trim() === b.toLowerCase().trim() +export function matchingPair(a, b) { + if (!nonEmptyString(a)) return false; + if (!nonEmptyString(b)) return false; + return a.toLowerCase().trim() === b.toLowerCase().trim(); } /** @@ -223,22 +217,26 @@ export function matchingPair (a, b) { * @param {any} addresses * @return {Array} */ -export function sortAddressesByStateAndCity (addresses) { +export function sortAddressesByStateAndCity(addresses) { return addresses.sort((a, b) => { - if (a.state < b.state) { return -1 } - if (a.state > b.state) { return 1 } - return a.city.localeCompare(b.city) - }) + if (a.state < b.state) { + return -1; + } + if (a.state > b.state) { + return 1; + } + return a.city.localeCompare(b.city); + }); } /** * Returns a SHA-1 hash of the profile */ -export async function hashObject (profile) { - const msgUint8 = new TextEncoder().encode(JSON.stringify(profile)) // encode as (utf-8) - const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8) // hash the message - const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string +export async function hashObject(profile) { + const msgUint8 = new TextEncoder().encode(JSON.stringify(profile)); // encode as (utf-8) + const hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8); // hash the message + const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string - return hashHex + return hashHex; } diff --git a/injected/src/features/click-to-load.js b/injected/src/features/click-to-load.js index 1b7763add..7110959a4 100644 --- a/injected/src/features/click-to-load.js +++ b/injected/src/features/click-to-load.js @@ -1,12 +1,12 @@ -import { Messaging, TestTransportConfig, WebkitMessagingConfig } from '../../../messaging/index.js' -import { createCustomEvent, originalWindowDispatchEvent } from '../utils.js' -import { logoImg, loadingImages, closeIcon, facebookLogo } from './click-to-load/ctl-assets.js' -import { getStyles, getConfig } from './click-to-load/ctl-config.js' -import { SendMessageMessagingTransport } from '../sendmessage-transport.js' -import ContentFeature from '../content-feature.js' -import { DDGCtlPlaceholderBlockedElement } from './click-to-load/components/ctl-placeholder-blocked.js' -import { DDGCtlLoginButton } from './click-to-load/components/ctl-login-button.js' -import { registerCustomElements } from './click-to-load/components' +import { Messaging, TestTransportConfig, WebkitMessagingConfig } from '../../../messaging/index.js'; +import { createCustomEvent, originalWindowDispatchEvent } from '../utils.js'; +import { logoImg, loadingImages, closeIcon, facebookLogo } from './click-to-load/ctl-assets.js'; +import { getStyles, getConfig } from './click-to-load/ctl-config.js'; +import { SendMessageMessagingTransport } from '../sendmessage-transport.js'; +import ContentFeature from '../content-feature.js'; +import { DDGCtlPlaceholderBlockedElement } from './click-to-load/components/ctl-placeholder-blocked.js'; +import { DDGCtlLoginButton } from './click-to-load/components/ctl-login-button.js'; +import { registerCustomElements } from './click-to-load/components'; /** * @typedef {'darkMode' | 'lightMode' | 'loginMode' | 'cancelMode'} displayMode @@ -17,78 +17,80 @@ import { registerCustomElements } from './click-to-load/components' * - `'cancelMode'`: Secondary colors styling for all themes */ -let devMode = false -let isYoutubePreviewsEnabled = false -let appID +let devMode = false; +let isYoutubePreviewsEnabled = false; +let appID; -const titleID = 'DuckDuckGoPrivacyEssentialsCTLElementTitle' +const titleID = 'DuckDuckGoPrivacyEssentialsCTLElementTitle'; // Configuration for how the placeholder elements should look and behave. // @see {getConfig} -let config = null -let sharedStrings = null -let styles = null +let config = null; +let sharedStrings = null; +let styles = null; /** * List of platforms where we can skip showing a Web Modal from C-S-S. * It is generally expected that the platform will show a native modal instead. * @type {import('../utils').Platform["name"][]} */ -const platformsWithNativeModalSupport = ['android', 'ios'] +const platformsWithNativeModalSupport = ['android', 'ios']; /** * Platforms supporting the new layout using Web Components. * @type {import('../utils').Platform["name"][]} */ -const platformsWithWebComponentsEnabled = ['android', 'ios'] +const platformsWithWebComponentsEnabled = ['android', 'ios']; /** * Based on the current Platform where the Widget is running, it will * return if it is one of our mobile apps or not. This should be used to * define which layout to use between Mobile and Desktop Platforms variations. * @type {import('../utils').Platform["name"][]} */ -const mobilePlatforms = ['android', 'ios'] +const mobilePlatforms = ['android', 'ios']; // TODO: Remove these redundant data structures and refactor the related code. // There should be no need to have the entity configuration stored in two // places. -const entities = [] -const entityData = {} +const entities = []; +const entityData = {}; // Used to avoid displaying placeholders for the same tracking element twice. -const knownTrackingElements = new WeakSet() +const knownTrackingElements = new WeakSet(); // Promise that is resolved when the Click to Load feature init() function has // finished its work, enough that it's now safe to replace elements with // placeholders. -let readyToDisplayPlaceholdersResolver -const readyToDisplayPlaceholders = new Promise(resolve => { - readyToDisplayPlaceholdersResolver = resolve -}) +let readyToDisplayPlaceholdersResolver; +const readyToDisplayPlaceholders = new Promise((resolve) => { + readyToDisplayPlaceholdersResolver = resolve; +}); // Promise that is resolved when the page has finished loading (and // readyToDisplayPlaceholders has resolved). Wait for this before sending // essential messages to surrogate scripts. -let afterPageLoadResolver -const afterPageLoad = new Promise(resolve => { afterPageLoadResolver = resolve }) +let afterPageLoadResolver; +const afterPageLoad = new Promise((resolve) => { + afterPageLoadResolver = resolve; +}); // Messaging layer for Click to Load. The messaging instance is initialized in // ClickToLoad.init() and updated here to be used outside ClickToLoad class // we need a module scoped reference. /** @type {import("@duckduckgo/messaging").Messaging} */ -let _messagingModuleScope +let _messagingModuleScope; /** @type function */ -let _addDebugFlag +let _addDebugFlag; const ctl = { /** * @return {import("@duckduckgo/messaging").Messaging} */ - get messaging () { - if (!_messagingModuleScope) throw new Error('Messaging not initialized') - return _messagingModuleScope + get messaging() { + if (!_messagingModuleScope) throw new Error('Messaging not initialized'); + return _messagingModuleScope; }, - addDebugFlag () { - if (!_addDebugFlag) throw new Error('addDebugFlag not initialized') - return _addDebugFlag() - } -} + addDebugFlag() { + if (!_addDebugFlag) throw new Error('addDebugFlag not initialized'); + return _addDebugFlag(); + }, +}; /********************************************************* * Widget Replacement logic @@ -104,19 +106,19 @@ class DuckWidget { * @param {import('../utils').Platform} platform * The platform where Click to Load and the Duck Widget is running on (ie Extension, Android App, etc) */ - constructor (widgetData, originalElement, entity, platform) { - this.clickAction = { ...widgetData.clickAction } // shallow copy - this.replaceSettings = widgetData.replaceSettings - this.originalElement = originalElement - this.placeholderElement = null - this.dataElements = {} - this.gatherDataElements() - this.entity = entity - this.widgetID = Math.random() - this.autoplay = false + constructor(widgetData, originalElement, entity, platform) { + this.clickAction = { ...widgetData.clickAction }; // shallow copy + this.replaceSettings = widgetData.replaceSettings; + this.originalElement = originalElement; + this.placeholderElement = null; + this.dataElements = {}; + this.gatherDataElements(); + this.entity = entity; + this.widgetID = Math.random(); + this.autoplay = false; // Boolean if widget is unblocked and content should not be blocked - this.isUnblocked = false - this.platform = platform + this.isUnblocked = false; + this.platform = platform; } /** @@ -125,18 +127,16 @@ class DuckWidget { * @param {EventTarget} eventTarget * @param {string} eventName */ - dispatchEvent (eventTarget, eventName) { + dispatchEvent(eventTarget, eventName) { eventTarget.dispatchEvent( - createCustomEvent( - eventName, { - detail: { - entity: this.entity, - replaceSettings: this.replaceSettings, - widgetID: this.widgetID - } - } - ) - ) + createCustomEvent(eventName, { + detail: { + entity: this.entity, + replaceSettings: this.replaceSettings, + widgetID: this.widgetID, + }, + }), + ); } /** @@ -144,47 +144,46 @@ class DuckWidget { * clickAction.urlDataAttributesToPreserve) and store those in * this.dataElement. */ - gatherDataElements () { + gatherDataElements() { if (!this.clickAction.urlDataAttributesToPreserve) { - return + return; } for (const [attrName, attrSettings] of Object.entries(this.clickAction.urlDataAttributesToPreserve)) { - let value = this.originalElement.getAttribute(attrName) + let value = this.originalElement.getAttribute(attrName); if (!value) { if (attrSettings.required) { // Missing a required attribute means we won't be able to replace it // with a light version, replace with full version. - this.clickAction.type = 'allowFull' + this.clickAction.type = 'allowFull'; } // If the attribute is "width", try first to measure the parent's width and use that as a default value. if (attrName === 'data-width') { - const windowWidth = window.innerWidth - const { parentElement } = this.originalElement - const parentStyles = parentElement - ? window.getComputedStyle(parentElement) - : null - let parentInnerWidth = null + const windowWidth = window.innerWidth; + const { parentElement } = this.originalElement; + const parentStyles = parentElement ? window.getComputedStyle(parentElement) : null; + let parentInnerWidth = null; // We want to calculate the inner width of the parent element as the iframe, when added back, // should not be bigger than the space available in the parent element. There is no straightforward way of // doing this. We need to get the parent's .clientWidth and remove the paddings size from it. if (parentElement && parentStyles && parentStyles.display !== 'inline') { - parentInnerWidth = parentElement.clientWidth - parseFloat(parentStyles.paddingLeft) - parseFloat(parentStyles.paddingRight) + parentInnerWidth = + parentElement.clientWidth - parseFloat(parentStyles.paddingLeft) - parseFloat(parentStyles.paddingRight); } if (parentInnerWidth && parentInnerWidth < windowWidth) { - value = parentInnerWidth.toString() + value = parentInnerWidth.toString(); } else { // Our default value for width is often greater than the window size of smaller // screens (ie mobile). Then use whatever is the smallest value. - value = Math.min(attrSettings.default, windowWidth).toString() + value = Math.min(attrSettings.default, windowWidth).toString(); } } else { - value = attrSettings.default + value = attrSettings.default; } } - this.dataElements[attrName] = value + this.dataElements[attrName] = value; } } @@ -193,26 +192,26 @@ class DuckWidget { * Load placeholder has been clicked by the user. * @returns {string} */ - getTargetURL () { + getTargetURL() { // Copying over data fields should be done lazily, since some required data may not be // captured until after page scripts run. - this.copySocialDataFields() - return this.clickAction.targetURL + this.copySocialDataFields(); + return this.clickAction.targetURL; } /** * Determines which display mode the placeholder element should render in. * @returns {displayMode} */ - getMode () { + getMode() { // Login buttons are always the login style types if (this.replaceSettings.type === 'loginButton') { - return 'loginMode' + return 'loginMode'; } if (window?.matchMedia('(prefers-color-scheme: dark)')?.matches) { - return 'darkMode' + return 'darkMode'; } - return 'lightMode' + return 'lightMode'; } /** @@ -221,29 +220,29 @@ class DuckWidget { * * @returns {string} */ - getStyle () { - let styleString = 'border: none;' + getStyle() { + let styleString = 'border: none;'; if (this.clickAction.styleDataAttributes) { // Copy elements from the original div into style attributes as directed by config for (const [attr, valAttr] of Object.entries(this.clickAction.styleDataAttributes)) { - let valueFound = this.dataElements[valAttr.name] + let valueFound = this.dataElements[valAttr.name]; if (!valueFound) { - valueFound = this.dataElements[valAttr.fallbackAttribute] + valueFound = this.dataElements[valAttr.fallbackAttribute]; } - let partialStyleString = '' + let partialStyleString = ''; if (valueFound) { - partialStyleString += `${attr}: ${valueFound}` + partialStyleString += `${attr}: ${valueFound}`; } if (!partialStyleString.includes(valAttr.unit)) { - partialStyleString += valAttr.unit + partialStyleString += valAttr.unit; } - partialStyleString += ';' - styleString += partialStyleString + partialStyleString += ';'; + styleString += partialStyleString; } } - return styleString + return styleString; } /** @@ -251,21 +250,21 @@ class DuckWidget { * placeholder element styling, and when restoring the original tracking * element. */ - copySocialDataFields () { + copySocialDataFields() { if (!this.clickAction.urlDataAttributesToPreserve) { - return + return; } // App ID may be set by client scripts, and is required for some elements. if (this.dataElements.app_id_replace && appID != null) { - this.clickAction.targetURL = this.clickAction.targetURL.replace('app_id_replace', appID) + this.clickAction.targetURL = this.clickAction.targetURL.replace('app_id_replace', appID); } for (const key of Object.keys(this.dataElements)) { - let attrValue = this.dataElements[key] + let attrValue = this.dataElements[key]; if (!attrValue) { - continue + continue; } // The URL for Facebook videos are specified as the data-href @@ -273,13 +272,10 @@ class DuckWidget { // Some websites omit the protocol part of the URL when doing // that, which then prevents the iframe from loading correctly. if (key === 'data-href' && attrValue.startsWith('//')) { - attrValue = window.location.protocol + attrValue + attrValue = window.location.protocol + attrValue; } - this.clickAction.targetURL = - this.clickAction.targetURL.replace( - key, encodeURIComponent(attrValue) - ) + this.clickAction.targetURL = this.clickAction.targetURL.replace(key, encodeURIComponent(attrValue)); } } @@ -288,13 +284,13 @@ class DuckWidget { * * @returns {HTMLIFrameElement} */ - createFBIFrame () { - const frame = document.createElement('iframe') + createFBIFrame() { + const frame = document.createElement('iframe'); - frame.setAttribute('src', this.getTargetURL()) - frame.setAttribute('style', this.getStyle()) + frame.setAttribute('src', this.getTargetURL()); + frame.setAttribute('style', this.getStyle()); - return frame + return frame; } /** @@ -305,14 +301,14 @@ class DuckWidget { * @returns {EventListener?} onError * Function to be called if the video fails to load. */ - adjustYouTubeVideoElement (videoElement) { - let onError = null + adjustYouTubeVideoElement(videoElement) { + let onError = null; if (!videoElement.src) { - return onError + return onError; } - const url = new URL(videoElement.src) - const { hostname: originalHostname } = url + const url = new URL(videoElement.src); + const { hostname: originalHostname } = url; // Upgrade video to YouTube's "privacy enhanced" mode, but fall back // to standard mode if the video fails to load. @@ -321,30 +317,30 @@ class DuckWidget { // violation on Chrome, see https://crbug.com/1271196. // 2. The onError event doesn't fire for blocked iframes on Chrome. if (originalHostname !== 'www.youtube-nocookie.com') { - url.hostname = 'www.youtube-nocookie.com' + url.hostname = 'www.youtube-nocookie.com'; onError = (event) => { - url.hostname = originalHostname - videoElement.src = url.href - event.stopImmediatePropagation() - } + url.hostname = originalHostname; + videoElement.src = url.href; + event.stopImmediatePropagation(); + }; } // Configure auto-play correctly depending on if the video's preview // loaded, otherwise it doesn't allow autoplay. - let allowString = videoElement.getAttribute('allow') || '' - const allowed = new Set(allowString.split(';').map(s => s.trim())) + let allowString = videoElement.getAttribute('allow') || ''; + const allowed = new Set(allowString.split(';').map((s) => s.trim())); if (this.autoplay) { - allowed.add('autoplay') - url.searchParams.set('autoplay', '1') + allowed.add('autoplay'); + url.searchParams.set('autoplay', '1'); } else { - allowed.delete('autoplay') - url.searchParams.delete('autoplay') + allowed.delete('autoplay'); + url.searchParams.delete('autoplay'); } - allowString = Array.from(allowed).join('; ') - videoElement.setAttribute('allow', allowString) + allowString = Array.from(allowed).join('; '); + videoElement.setAttribute('allow', allowString); - videoElement.src = url.href - return onError + videoElement.src = url.href; + return onError; } /** @@ -358,19 +354,19 @@ class DuckWidget { * @returns {Promise} * Promise that resolves when the fade in/out is complete. */ - fadeElement (element, interval, fadeIn) { - return new Promise(resolve => { - let opacity = fadeIn ? 0 : 1 - const originStyle = element.style.cssText + fadeElement(element, interval, fadeIn) { + return new Promise((resolve) => { + let opacity = fadeIn ? 0 : 1; + const originStyle = element.style.cssText; const fadeOut = setInterval(function () { - opacity += fadeIn ? 0.03 : -0.03 - element.style.cssText = originStyle + `opacity: ${opacity};` + opacity += fadeIn ? 0.03 : -0.03; + element.style.cssText = originStyle + `opacity: ${opacity};`; if (opacity <= 0 || opacity >= 1) { - clearInterval(fadeOut) - resolve() + clearInterval(fadeOut); + resolve(); } - }, interval) - }) + }, interval); + }); } /** @@ -380,8 +376,8 @@ class DuckWidget { * @returns {Promise} * Promise that resolves when the fade out is complete. */ - fadeOutElement (element) { - return this.fadeElement(element, 10, false) + fadeOutElement(element) { + return this.fadeElement(element, 10, false); } /** @@ -391,8 +387,8 @@ class DuckWidget { * @returns {Promise} * Promise that resolves when the fade in is complete. */ - fadeInElement (element) { - return this.fadeElement(element, 10, true) + fadeInElement(element) { + return this.fadeElement(element, 10, true); } /** @@ -404,124 +400,126 @@ class DuckWidget { * @param {HTMLElement} replacementElement * The placeholder element. */ - clickFunction (originalElement, replacementElement) { - let clicked = false - const handleClick = e => { + clickFunction(originalElement, replacementElement) { + let clicked = false; + const handleClick = (e) => { // Ensure that the click is created by a user event & prevent double clicks from adding more animations if (e.isTrusted && !clicked) { - e.stopPropagation() - this.isUnblocked = true - clicked = true - let isLogin = false + e.stopPropagation(); + this.isUnblocked = true; + clicked = true; + let isLogin = false; // Logins triggered by user click means they were not triggered by the surrogate - const isSurrogateLogin = false - const clickElement = e.srcElement // Object.assign({}, e) + const isSurrogateLogin = false; + const clickElement = e.srcElement; // Object.assign({}, e) if (this.replaceSettings.type === 'loginButton') { - isLogin = true + isLogin = true; } - const action = this.entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb' + const action = this.entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'; // eslint-disable-next-line promise/prefer-await-to-then unblockClickToLoadContent({ entity: this.entity, action, isLogin, isSurrogateLogin }).then((response) => { // If user rejected confirmation modal and content was not unblocked, inform surrogate and stop. if (response && response.type === 'ddg-ctp-user-cancel') { - return abortSurrogateConfirmation(this.entity) + return abortSurrogateConfirmation(this.entity); } - const parent = replacementElement.parentNode + const parent = replacementElement.parentNode; // The placeholder was removed from the DOM while we loaded // the original content, give up. - if (!parent) return + if (!parent) return; // If we allow everything when this element is clicked, // notify surrogate to enable SDK and replace original element. if (this.clickAction.type === 'allowFull') { - parent.replaceChild(originalElement, replacementElement) - this.dispatchEvent(window, 'ddg-ctp-load-sdk') - return + parent.replaceChild(originalElement, replacementElement); + this.dispatchEvent(window, 'ddg-ctp-load-sdk'); + return; } // Create a container for the new FB element - const fbContainer = document.createElement('div') - fbContainer.style.cssText = styles.wrapperDiv - const fadeIn = document.createElement('div') - fadeIn.style.cssText = 'display: none; opacity: 0;' + const fbContainer = document.createElement('div'); + fbContainer.style.cssText = styles.wrapperDiv; + const fadeIn = document.createElement('div'); + fadeIn.style.cssText = 'display: none; opacity: 0;'; // Loading animation (FB can take some time to load) - const loadingImg = document.createElement('img') - loadingImg.setAttribute('src', loadingImages[this.getMode()]) - loadingImg.setAttribute('height', '14px') - loadingImg.style.cssText = styles.loadingImg + const loadingImg = document.createElement('img'); + loadingImg.setAttribute('src', loadingImages[this.getMode()]); + loadingImg.setAttribute('height', '14px'); + loadingImg.style.cssText = styles.loadingImg; // Always add the animation to the button, regardless of click source if (clickElement.nodeName === 'BUTTON') { - clickElement.firstElementChild.insertBefore(loadingImg, clickElement.firstElementChild.firstChild) + clickElement.firstElementChild.insertBefore(loadingImg, clickElement.firstElementChild.firstChild); } else { // try to find the button - let el = clickElement - let button = null + let el = clickElement; + let button = null; while (button === null && el !== null) { - button = el.querySelector('button') - el = el.parentElement + button = el.querySelector('button'); + el = el.parentElement; } if (button) { - button.firstElementChild.insertBefore(loadingImg, button.firstElementChild.firstChild) + button.firstElementChild.insertBefore(loadingImg, button.firstElementChild.firstChild); } } - fbContainer.appendChild(fadeIn) + fbContainer.appendChild(fadeIn); - let fbElement - let onError = null + let fbElement; + let onError = null; switch (this.clickAction.type) { - case 'iFrame': - fbElement = this.createFBIFrame() - break - case 'youtube-video': - onError = this.adjustYouTubeVideoElement(originalElement) - fbElement = originalElement - break - default: - fbElement = originalElement - break + case 'iFrame': + fbElement = this.createFBIFrame(); + break; + case 'youtube-video': + onError = this.adjustYouTubeVideoElement(originalElement); + fbElement = originalElement; + break; + default: + fbElement = originalElement; + break; } // Modify the overlay to include a Facebook iFrame, which // starts invisible. Once loaded, fade out and remove the // overlay then fade in the Facebook content. - parent.replaceChild(fbContainer, replacementElement) - fbContainer.appendChild(replacementElement) - fadeIn.appendChild(fbElement) - fbElement.addEventListener('load', async () => { - await this.fadeOutElement(replacementElement) - fbContainer.replaceWith(fbElement) - this.dispatchEvent(fbElement, 'ddg-ctp-placeholder-clicked') - await this.fadeInElement(fadeIn) - // Focus on new element for screen readers. - fbElement.focus() - }, { once: true }) + parent.replaceChild(fbContainer, replacementElement); + fbContainer.appendChild(replacementElement); + fadeIn.appendChild(fbElement); + fbElement.addEventListener( + 'load', + async () => { + await this.fadeOutElement(replacementElement); + fbContainer.replaceWith(fbElement); + this.dispatchEvent(fbElement, 'ddg-ctp-placeholder-clicked'); + await this.fadeInElement(fadeIn); + // Focus on new element for screen readers. + fbElement.focus(); + }, + { once: true }, + ); // Note: This event only fires on Firefox, on Chrome the frame's // load event will always fire. if (onError) { - fbElement.addEventListener('error', onError, { once: true }) + fbElement.addEventListener('error', onError, { once: true }); } - }) + }); } - } + }; // If this is a login button, show modal if needed if (this.replaceSettings.type === 'loginButton' && entityData[this.entity].shouldShowLoginModal) { - return e => { + return (e) => { // Even if the user cancels the login attempt, consider Facebook Click to // Load to have been active on the page if the user reports the page as broken. if (this.entity === 'Facebook, Inc.') { - notifyFacebookLogin() + notifyFacebookLogin(); } - handleUnblockConfirmation( - this.platform.name, this.entity, handleClick, e - ) - } + handleUnblockConfirmation(this.platform.name, this.entity, handleClick, e); + }; } - return handleClick + return handleClick; } /** @@ -529,8 +527,8 @@ class DuckWidget { * return if the new layout using Web Components is supported or not. * @returns {boolean} */ - shouldUseCustomElement () { - return platformsWithWebComponentsEnabled.includes(this.platform.name) + shouldUseCustomElement() { + return platformsWithWebComponentsEnabled.includes(this.platform.name); } /** @@ -539,8 +537,8 @@ class DuckWidget { * define which layout to use between Mobile and Desktop Platforms variations. * @returns {boolean} */ - isMobilePlatform () { - return mobilePlatforms.includes(this.platform.name) + isMobilePlatform() { + return mobilePlatforms.includes(this.platform.name); } } @@ -574,28 +572,23 @@ class DuckWidget { * @param {HTMLElement} placeholderElement * The placeholder element that should be shown instead. */ -function replaceTrackingElement (widget, trackingElement, placeholderElement) { +function replaceTrackingElement(widget, trackingElement, placeholderElement) { // In some situations (e.g. YouTube Click to Load previews are // enabled/disabled), a second placeholder will be shown for a tracking // element. - const elementToReplace = widget.placeholderElement || trackingElement + const elementToReplace = widget.placeholderElement || trackingElement; // Note the placeholder element, so that it can also be replaced later if // necessary. - widget.placeholderElement = placeholderElement + widget.placeholderElement = placeholderElement; // First hide the element, since we need to keep it in the DOM until the // events have been dispatched. - const originalDisplay = [ - elementToReplace.style.getPropertyValue('display'), - elementToReplace.style.getPropertyPriority('display') - ] - elementToReplace.style.setProperty('display', 'none', 'important') + const originalDisplay = [elementToReplace.style.getPropertyValue('display'), elementToReplace.style.getPropertyPriority('display')]; + elementToReplace.style.setProperty('display', 'none', 'important'); // Add the placeholder element to the page. - elementToReplace.parentElement.insertBefore( - placeholderElement, elementToReplace - ) + elementToReplace.parentElement.insertBefore(placeholderElement, elementToReplace); // While the placeholder is shown (and original element hidden) // synchronously, the events are dispatched (and original element removed @@ -604,14 +597,14 @@ function replaceTrackingElement (widget, trackingElement, placeholderElement) { afterPageLoad.then(() => { // With page load complete, and both elements in the DOM, the events can // be dispatched. - widget.dispatchEvent(trackingElement, 'ddg-ctp-tracking-element') - widget.dispatchEvent(placeholderElement, 'ddg-ctp-placeholder-element') + widget.dispatchEvent(trackingElement, 'ddg-ctp-tracking-element'); + widget.dispatchEvent(placeholderElement, 'ddg-ctp-placeholder-element'); // Once the events are sent, the tracking element (or previous // placeholder) can finally be removed from the DOM. - elementToReplace.remove() - elementToReplace.style.setProperty('display', ...originalDisplay) - }) + elementToReplace.remove(); + elementToReplace.style.setProperty('display', ...originalDisplay); + }); } /** @@ -622,13 +615,13 @@ function replaceTrackingElement (widget, trackingElement, placeholderElement) { * @param {HTMLIFrameElement} trackingElement * The tracking element on the page that should be replaced with a placeholder. */ -function createPlaceholderElementAndReplace (widget, trackingElement) { +function createPlaceholderElementAndReplace(widget, trackingElement) { if (widget.replaceSettings.type === 'blank') { - replaceTrackingElement(widget, trackingElement, document.createElement('div')) + replaceTrackingElement(widget, trackingElement, document.createElement('div')); } if (widget.replaceSettings.type === 'loginButton') { - const icon = widget.replaceSettings.icon + const icon = widget.replaceSettings.icon; // Create a button to replace old element if (widget.shouldUseCustomElement()) { const facebookLoginButton = new DDGCtlLoginButton({ @@ -637,29 +630,33 @@ function createPlaceholderElementAndReplace (widget, trackingElement) { hoverText: widget.replaceSettings.popupBodyText, logoIcon: facebookLogo, originalElement: trackingElement, - learnMore: { // Localized strings for "Learn More" link. + learnMore: { + // Localized strings for "Learn More" link. readAbout: sharedStrings.readAbout, - learnMore: sharedStrings.learnMore + learnMore: sharedStrings.learnMore, }, - onClick: widget.clickFunction.bind(widget) - }).element - facebookLoginButton.classList.add('fb-login-button', 'FacebookLogin__button') - facebookLoginButton.appendChild(makeFontFaceStyleElement()) - replaceTrackingElement(widget, trackingElement, facebookLoginButton) + onClick: widget.clickFunction.bind(widget), + }).element; + facebookLoginButton.classList.add('fb-login-button', 'FacebookLogin__button'); + facebookLoginButton.appendChild(makeFontFaceStyleElement()); + replaceTrackingElement(widget, trackingElement, facebookLoginButton); } else { const { button, container } = makeLoginButton( - widget.replaceSettings.buttonText, widget.getMode(), - widget.replaceSettings.popupBodyText, icon, trackingElement - ) - button.addEventListener('click', widget.clickFunction(trackingElement, container)) - replaceTrackingElement(widget, trackingElement, container) + widget.replaceSettings.buttonText, + widget.getMode(), + widget.replaceSettings.popupBodyText, + icon, + trackingElement, + ); + button.addEventListener('click', widget.clickFunction(trackingElement, container)); + replaceTrackingElement(widget, trackingElement, container); } } // Facebook if (widget.replaceSettings.type === 'dialog') { - ctl.addDebugFlag() - ctl.messaging.notify('updateFacebookCTLBreakageFlags', { ctlFacebookPlaceholderShown: true }) + ctl.addDebugFlag(); + ctl.messaging.notify('updateFacebookCTLBreakageFlags', { ctlFacebookPlaceholderShown: true }); if (widget.shouldUseCustomElement()) { /** * Creates a custom HTML element with the placeholder element for blocked @@ -673,46 +670,42 @@ function createPlaceholderElementAndReplace (widget, trackingElement) { unblockBtnText: widget.replaceSettings.buttonText, // Unblock button text useSlimCard: false, // Flag for using less padding on card (ie YT CTL on mobile) originalElement: trackingElement, // The original element this placeholder is replacing. - learnMore: { // Localized strings for "Learn More" link. + learnMore: { + // Localized strings for "Learn More" link. readAbout: sharedStrings.readAbout, - learnMore: sharedStrings.learnMore + learnMore: sharedStrings.learnMore, }, - onButtonClick: widget.clickFunction.bind(widget) - }) - mobileBlockedPlaceholder.appendChild(makeFontFaceStyleElement()) + onButtonClick: widget.clickFunction.bind(widget), + }); + mobileBlockedPlaceholder.appendChild(makeFontFaceStyleElement()); - replaceTrackingElement(widget, trackingElement, mobileBlockedPlaceholder) - showExtraUnblockIfShortPlaceholder(null, mobileBlockedPlaceholder) + replaceTrackingElement(widget, trackingElement, mobileBlockedPlaceholder); + showExtraUnblockIfShortPlaceholder(null, mobileBlockedPlaceholder); } else { - const icon = widget.replaceSettings.icon - const button = makeButton(widget.replaceSettings.buttonText, widget.getMode()) - const textButton = makeTextButton(widget.replaceSettings.buttonText, widget.getMode()) - const { contentBlock, shadowRoot } = createContentBlock( - widget, button, textButton, icon - ) - button.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)) - textButton.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)) - - replaceTrackingElement(widget, trackingElement, contentBlock) - showExtraUnblockIfShortPlaceholder(shadowRoot, contentBlock) + const icon = widget.replaceSettings.icon; + const button = makeButton(widget.replaceSettings.buttonText, widget.getMode()); + const textButton = makeTextButton(widget.replaceSettings.buttonText, widget.getMode()); + const { contentBlock, shadowRoot } = createContentBlock(widget, button, textButton, icon); + button.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)); + textButton.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)); + + replaceTrackingElement(widget, trackingElement, contentBlock); + showExtraUnblockIfShortPlaceholder(shadowRoot, contentBlock); } } // YouTube if (widget.replaceSettings.type === 'youtube-video') { - ctl.addDebugFlag() - ctl.messaging.notify('updateYouTubeCTLAddedFlag', { youTubeCTLAddedFlag: true }) - replaceYouTubeCTL(trackingElement, widget) + ctl.addDebugFlag(); + ctl.messaging.notify('updateYouTubeCTLAddedFlag', { youTubeCTLAddedFlag: true }); + replaceYouTubeCTL(trackingElement, widget); // Subscribe to changes to youtubePreviewsEnabled setting // and update the CTL state - ctl.messaging.subscribe( - 'setYoutubePreviewsEnabled', - ({ value }) => { - isYoutubePreviewsEnabled = value - replaceYouTubeCTL(trackingElement, widget) - } - ) + ctl.messaging.subscribe('setYoutubePreviewsEnabled', ({ value }) => { + isYoutubePreviewsEnabled = value; + replaceYouTubeCTL(trackingElement, widget); + }); } } @@ -722,23 +715,23 @@ function createPlaceholderElementAndReplace (widget, trackingElement) { * @param {DuckWidget} widget * The CTL 'widget' associated with the tracking element. */ -function replaceYouTubeCTL (trackingElement, widget) { +function replaceYouTubeCTL(trackingElement, widget) { // Skip replacing tracking element if it has already been unblocked if (widget.isUnblocked) { - return + return; } if (isYoutubePreviewsEnabled === true) { // Show YouTube Preview for embedded video - const oldPlaceholder = widget.placeholderElement - const { youTubePreview, shadowRoot } = createYouTubePreview(trackingElement, widget) - resizeElementToMatch(oldPlaceholder || trackingElement, youTubePreview) - replaceTrackingElement(widget, trackingElement, youTubePreview) - showExtraUnblockIfShortPlaceholder(shadowRoot, youTubePreview) + const oldPlaceholder = widget.placeholderElement; + const { youTubePreview, shadowRoot } = createYouTubePreview(trackingElement, widget); + resizeElementToMatch(oldPlaceholder || trackingElement, youTubePreview); + replaceTrackingElement(widget, trackingElement, youTubePreview); + showExtraUnblockIfShortPlaceholder(shadowRoot, youTubePreview); } else { // Block YouTube embedded video and display blocking dialog - widget.autoplay = false - const oldPlaceholder = widget.placeholderElement + widget.autoplay = false; + const oldPlaceholder = widget.placeholderElement; if (widget.shouldUseCustomElement()) { /** @@ -753,34 +746,36 @@ function replaceYouTubeCTL (trackingElement, widget) { unblockBtnText: widget.replaceSettings.buttonText, // Unblock button text useSlimCard: true, // Flag for using less padding on card (ie YT CTL on mobile) originalElement: trackingElement, // The original element this placeholder is replacing. - learnMore: { // Localized strings for "Learn More" link. + learnMore: { + // Localized strings for "Learn More" link. readAbout: sharedStrings.readAbout, - learnMore: sharedStrings.learnMore + learnMore: sharedStrings.learnMore, }, - withToggle: { // Toggle config to be displayed in the bottom of the placeholder + withToggle: { + // Toggle config to be displayed in the bottom of the placeholder isActive: false, // Toggle state dataKey: 'yt-preview-toggle', // data-key attribute for button label: widget.replaceSettings.previewToggleText, // Text to be presented with toggle size: widget.isMobilePlatform() ? 'lg' : 'md', - onClick: () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: true }) // Toggle click callback + onClick: () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: true }), // Toggle click callback }, withFeedback: { label: sharedStrings.shareFeedback, - onClick: () => openShareFeedbackPage() + onClick: () => openShareFeedbackPage(), }, - onButtonClick: widget.clickFunction.bind(widget) - }) - mobileBlockedPlaceholderElement.appendChild(makeFontFaceStyleElement()) - mobileBlockedPlaceholderElement.id = trackingElement.id - resizeElementToMatch(oldPlaceholder || trackingElement, mobileBlockedPlaceholderElement) - replaceTrackingElement(widget, trackingElement, mobileBlockedPlaceholderElement) - showExtraUnblockIfShortPlaceholder(null, mobileBlockedPlaceholderElement) + onButtonClick: widget.clickFunction.bind(widget), + }); + mobileBlockedPlaceholderElement.appendChild(makeFontFaceStyleElement()); + mobileBlockedPlaceholderElement.id = trackingElement.id; + resizeElementToMatch(oldPlaceholder || trackingElement, mobileBlockedPlaceholderElement); + replaceTrackingElement(widget, trackingElement, mobileBlockedPlaceholderElement); + showExtraUnblockIfShortPlaceholder(null, mobileBlockedPlaceholderElement); } else { - const { blockingDialog, shadowRoot } = createYouTubeBlockingDialog(trackingElement, widget) - resizeElementToMatch(oldPlaceholder || trackingElement, blockingDialog) - replaceTrackingElement(widget, trackingElement, blockingDialog) - showExtraUnblockIfShortPlaceholder(shadowRoot, blockingDialog) - hideInfoTextIfNarrowPlaceholder(shadowRoot, blockingDialog, 460) + const { blockingDialog, shadowRoot } = createYouTubeBlockingDialog(trackingElement, widget); + resizeElementToMatch(oldPlaceholder || trackingElement, blockingDialog); + replaceTrackingElement(widget, trackingElement, blockingDialog); + showExtraUnblockIfShortPlaceholder(shadowRoot, blockingDialog); + hideInfoTextIfNarrowPlaceholder(shadowRoot, blockingDialog, 460); } } } @@ -793,40 +788,37 @@ function replaceYouTubeCTL (trackingElement, widget) { * @param {ShadowRoot?} shadowRoot * @param {HTMLElement} placeholder Placeholder for tracking element */ -function showExtraUnblockIfShortPlaceholder (shadowRoot, placeholder) { +function showExtraUnblockIfShortPlaceholder(shadowRoot, placeholder) { if (!placeholder.parentElement) { - return + return; } - const parentStyles = window.getComputedStyle(placeholder.parentElement) + const parentStyles = window.getComputedStyle(placeholder.parentElement); // Inline elements, like span or p, don't have a height value that we can use because they're // not a "block" like element with defined sizes. Because we skip this check on "inline" // parents, it might be necessary to traverse up the DOM tree until we find the nearest non // "inline" parent to get a reliable height for this check. if (parentStyles.display === 'inline') { - return + return; } - const { height: placeholderHeight } = placeholder.getBoundingClientRect() - const { height: parentHeight } = placeholder.parentElement.getBoundingClientRect() + const { height: placeholderHeight } = placeholder.getBoundingClientRect(); + const { height: parentHeight } = placeholder.parentElement.getBoundingClientRect(); - if ( - (placeholderHeight > 0 && placeholderHeight <= 200) || - (parentHeight > 0 && parentHeight <= 230) - ) { + if ((placeholderHeight > 0 && placeholderHeight <= 200) || (parentHeight > 0 && parentHeight <= 230)) { if (shadowRoot) { /** @type {HTMLElement?} */ - const titleRowTextButton = shadowRoot.querySelector(`#${titleID + 'TextButton'}`) + const titleRowTextButton = shadowRoot.querySelector(`#${titleID + 'TextButton'}`); if (titleRowTextButton) { - titleRowTextButton.style.display = 'block' + titleRowTextButton.style.display = 'block'; } } // Avoid the placeholder being taller than the containing element // and overflowing. /** @type {HTMLElement?} */ - const blockedDiv = shadowRoot?.querySelector('.DuckDuckGoSocialContainer') || placeholder + const blockedDiv = shadowRoot?.querySelector('.DuckDuckGoSocialContainer') || placeholder; if (blockedDiv) { - blockedDiv.style.minHeight = 'initial' - blockedDiv.style.maxHeight = parentHeight + 'px' - blockedDiv.style.overflow = 'hidden' + blockedDiv.style.minHeight = 'initial'; + blockedDiv.style.maxHeight = parentHeight + 'px'; + blockedDiv.style.overflow = 'hidden'; } } } @@ -840,32 +832,31 @@ function showExtraUnblockIfShortPlaceholder (shadowRoot, placeholder) { * Maximum placeholder width (in pixels) for the placeholder to be considered * narrow. */ -function hideInfoTextIfNarrowPlaceholder (shadowRoot, placeholder, narrowWidth) { - const { width: placeholderWidth } = placeholder.getBoundingClientRect() +function hideInfoTextIfNarrowPlaceholder(shadowRoot, placeholder, narrowWidth) { + const { width: placeholderWidth } = placeholder.getBoundingClientRect(); if (placeholderWidth > 0 && placeholderWidth <= narrowWidth) { - const buttonContainer = - shadowRoot.querySelector('.DuckDuckGoButton.primary')?.parentElement - const contentTitle = shadowRoot.getElementById('contentTitle') - const infoText = shadowRoot.getElementById('infoText') + const buttonContainer = shadowRoot.querySelector('.DuckDuckGoButton.primary')?.parentElement; + const contentTitle = shadowRoot.getElementById('contentTitle'); + const infoText = shadowRoot.getElementById('infoText'); /** @type {HTMLElement?} */ - const learnMoreLink = shadowRoot.getElementById('learnMoreLink') + const learnMoreLink = shadowRoot.getElementById('learnMoreLink'); // These elements will exist, but this check keeps TypeScript happy. if (!buttonContainer || !contentTitle || !infoText || !learnMoreLink) { - return + return; } // Remove the information text. - infoText.remove() - learnMoreLink.remove() + infoText.remove(); + learnMoreLink.remove(); // Append the "Learn More" link to the title. - contentTitle.innerText += '. ' - learnMoreLink.style.removeProperty('font-size') - contentTitle.appendChild(learnMoreLink) + contentTitle.innerText += '. '; + learnMoreLink.style.removeProperty('font-size'); + contentTitle.appendChild(learnMoreLink); // Improve margin/padding, to ensure as much is displayed as possible. - buttonContainer.style.removeProperty('margin') + buttonContainer.style.removeProperty('margin'); } } @@ -896,8 +887,8 @@ function hideInfoTextIfNarrowPlaceholder (shadowRoot, placeholder, narrowWidth) * @see {@link ddg-ctp-unblockClickToLoadContent-complete} for the response handler. * @returns {Promise} */ -function unblockClickToLoadContent (message) { - return ctl.messaging.request('unblockClickToLoadContent', message) +function unblockClickToLoadContent(message) { + return ctl.messaging.request('unblockClickToLoadContent', message); } /** @@ -914,16 +905,16 @@ function unblockClickToLoadContent (message) { * @param {...any} acceptFunctionParams * The parameters passed to acceptFunction when it is called. */ -function handleUnblockConfirmation (platformName, entity, acceptFunction, ...acceptFunctionParams) { +function handleUnblockConfirmation(platformName, entity, acceptFunction, ...acceptFunctionParams) { // In our mobile platforms, we want to show a native UI to request user unblock // confirmation. In these cases we send directly the unblock request to the platform // and the platform chooses how to best handle it. if (platformsWithNativeModalSupport.includes(platformName)) { - acceptFunction(...acceptFunctionParams) - // By default, for other platforms (ie Extension), we show a web modal with a - // confirmation request to the user before we proceed to unblock the content. + acceptFunction(...acceptFunctionParams); + // By default, for other platforms (ie Extension), we show a web modal with a + // confirmation request to the user before we proceed to unblock the content. } else { - makeModal(entity, acceptFunction, ...acceptFunctionParams) + makeModal(entity, acceptFunction, ...acceptFunctionParams); } } @@ -932,9 +923,9 @@ function handleUnblockConfirmation (platformName, entity, acceptFunction, ...acc * Facebook Click to Load login flow had started if the user should then report * the website as broken. */ -function notifyFacebookLogin () { - ctl.addDebugFlag() - ctl.messaging.notify('updateFacebookCTLBreakageFlags', { ctlFacebookLogin: true }) +function notifyFacebookLogin() { + ctl.addDebugFlag(); + ctl.messaging.notify('updateFacebookCTLBreakageFlags', { ctlFacebookLogin: true }); } /** @@ -943,25 +934,25 @@ function notifyFacebookLogin () { * shown. * @param {string} entity */ -async function runLogin (entity) { +async function runLogin(entity) { if (entity === 'Facebook, Inc.') { - notifyFacebookLogin() + notifyFacebookLogin(); } - const action = entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb' - const response = await unblockClickToLoadContent({ entity, action, isLogin: true, isSurrogateLogin: true }) + const action = entity === 'Youtube' ? 'block-ctl-yt' : 'block-ctl-fb'; + const response = await unblockClickToLoadContent({ entity, action, isLogin: true, isSurrogateLogin: true }); // If user rejected confirmation modal and content was not unblocked, inform surrogate and stop. if (response && response.type === 'ddg-ctp-user-cancel') { - return abortSurrogateConfirmation(this.entity) + return abortSurrogateConfirmation(this.entity); } // Communicate with surrogate to run login originalWindowDispatchEvent( createCustomEvent('ddg-ctp-run-login', { detail: { - entity - } - }) - ) + entity, + }, + }), + ); } /** @@ -969,18 +960,18 @@ async function runLogin (entity) { * Called after the user cancel from a warning dialog. * @param {string} entity */ -function abortSurrogateConfirmation (entity) { +function abortSurrogateConfirmation(entity) { originalWindowDispatchEvent( createCustomEvent('ddg-ctp-cancel-modal', { detail: { - entity - } - }) - ) + entity, + }, + }), + ); } -function openShareFeedbackPage () { - ctl.messaging.notify('openShareFeedbackPage') +function openShareFeedbackPage() { + ctl.messaging.notify('openShareFeedbackPage'); } /********************************************************* @@ -992,15 +983,15 @@ function openShareFeedbackPage () { * @param {displayMode} [mode='lightMode'] * @returns {HTMLAnchorElement} */ -function getLearnMoreLink (mode = 'lightMode') { - const linkElement = document.createElement('a') - linkElement.style.cssText = styles.generalLink + styles[mode].linkFont - linkElement.ariaLabel = sharedStrings.readAbout - linkElement.href = 'https://help.duckduckgo.com/duckduckgo-help-pages/privacy/embedded-content-protection/' - linkElement.target = '_blank' - linkElement.textContent = sharedStrings.learnMore - linkElement.id = 'learnMoreLink' - return linkElement +function getLearnMoreLink(mode = 'lightMode') { + const linkElement = document.createElement('a'); + linkElement.style.cssText = styles.generalLink + styles[mode].linkFont; + linkElement.ariaLabel = sharedStrings.readAbout; + linkElement.href = 'https://help.duckduckgo.com/duckduckgo-help-pages/privacy/embedded-content-protection/'; + linkElement.target = '_blank'; + linkElement.textContent = sharedStrings.learnMore; + linkElement.id = 'learnMoreLink'; + return linkElement; } /** @@ -1008,10 +999,9 @@ function getLearnMoreLink (mode = 'lightMode') { * @param {HTMLElement} sourceElement * @param {HTMLElement} targetElement */ -function resizeElementToMatch (sourceElement, targetElement) { - const computedStyle = window.getComputedStyle(sourceElement) - const stylesToCopy = ['position', 'top', 'bottom', 'left', 'right', - 'transform', 'margin'] +function resizeElementToMatch(sourceElement, targetElement) { + const computedStyle = window.getComputedStyle(sourceElement); + const stylesToCopy = ['position', 'top', 'bottom', 'left', 'right', 'transform', 'margin']; // It's apparently preferable to use the source element's size relative to // the current viewport, when resizing the target element. However, the @@ -1019,26 +1009,26 @@ function resizeElementToMatch (sourceElement, targetElement) { // that happens, getBoundingClientRect will return all zeros. // TODO: Remove this entirely, and always use the computed height/width of // the source element instead? - const { height, width } = sourceElement.getBoundingClientRect() + const { height, width } = sourceElement.getBoundingClientRect(); if (height > 0 && width > 0) { - targetElement.style.height = height + 'px' - targetElement.style.width = width + 'px' + targetElement.style.height = height + 'px'; + targetElement.style.width = width + 'px'; } else { - stylesToCopy.push('height', 'width') + stylesToCopy.push('height', 'width'); } for (const key of stylesToCopy) { - targetElement.style[key] = computedStyle[key] + targetElement.style[key] = computedStyle[key]; } // If the parent element is very small (and its dimensions can be trusted) set a max height/width // to avoid the placeholder overflowing. if (computedStyle.display !== 'inline') { if (targetElement.style.maxHeight < computedStyle.height) { - targetElement.style.maxHeight = 'initial' + targetElement.style.maxHeight = 'initial'; } if (targetElement.style.maxWidth < computedStyle.width) { - targetElement.style.maxWidth = 'initial' + targetElement.style.maxWidth = 'initial'; } } } @@ -1048,13 +1038,13 @@ function resizeElementToMatch (sourceElement, targetElement) { * to be attached to DDG wrapper elements * @returns HTMLStyleElement */ -function makeFontFaceStyleElement () { +function makeFontFaceStyleElement() { // Put our custom font-faces inside the wrapper element, since // @font-face does not work inside a shadowRoot. // See https://github.com/mdn/interactive-examples/issues/887. - const fontFaceStyleElement = document.createElement('style') - fontFaceStyleElement.textContent = styles.fontStyle - return fontFaceStyleElement + const fontFaceStyleElement = document.createElement('style'); + fontFaceStyleElement.textContent = styles.fontStyle; + return fontFaceStyleElement; } /** @@ -1064,10 +1054,10 @@ function makeFontFaceStyleElement () { * @param {displayMode} [mode='lightMode'] * @returns {{wrapperClass: string, styleElement: HTMLStyleElement; }} */ -function makeBaseStyleElement (mode = 'lightMode') { +function makeBaseStyleElement(mode = 'lightMode') { // Style element includes our font & overwrites page styles - const styleElement = document.createElement('style') - const wrapperClass = 'DuckDuckGoSocialContainer' + const styleElement = document.createElement('style'); + const wrapperClass = 'DuckDuckGoSocialContainer'; styleElement.textContent = ` .${wrapperClass} a { ${styles[mode].linkFont} @@ -1107,8 +1097,8 @@ function makeBaseStyleElement (mode = 'lightMode') { .DuckDuckGoButton.secondary:active { ${styles.cancelMode.buttonBackgroundPress} } - ` - return { wrapperClass, styleElement } + `; + return { wrapperClass, styleElement }; } /** @@ -1118,11 +1108,11 @@ function makeBaseStyleElement (mode = 'lightMode') { * @param {displayMode} mode * @returns {HTMLAnchorElement} */ -function makeTextButton (linkText, mode = 'lightMode') { - const linkElement = document.createElement('a') - linkElement.style.cssText = styles.headerLink + styles[mode].linkFont - linkElement.textContent = linkText - return linkElement +function makeTextButton(linkText, mode = 'lightMode') { + const linkElement = document.createElement('a'); + linkElement.style.cssText = styles.headerLink + styles[mode].linkFont; + linkElement.textContent = linkText; + return linkElement; } /** @@ -1135,16 +1125,16 @@ function makeTextButton (linkText, mode = 'lightMode') { * action. * @returns {HTMLButtonElement} Button element */ -function makeButton (buttonText, mode = 'lightMode') { - const button = document.createElement('button') - button.classList.add('DuckDuckGoButton') - button.classList.add(mode === 'cancelMode' ? 'secondary' : 'primary') +function makeButton(buttonText, mode = 'lightMode') { + const button = document.createElement('button'); + button.classList.add('DuckDuckGoButton'); + button.classList.add(mode === 'cancelMode' ? 'secondary' : 'primary'); if (buttonText) { - const textContainer = document.createElement('div') - textContainer.textContent = buttonText - button.appendChild(textContainer) + const textContainer = document.createElement('div'); + textContainer.textContent = buttonText; + button.appendChild(textContainer); } - return button + return button; } /** @@ -1158,28 +1148,26 @@ function makeButton (buttonText, mode = 'lightMode') { * Value to assign to the button's 'data-key' attribute. * @returns {HTMLButtonElement} */ -function makeToggleButton (mode, isActive = false, classNames = '', dataKey = '') { - const toggleButton = document.createElement('button') - toggleButton.className = classNames - toggleButton.style.cssText = styles.toggleButton - toggleButton.type = 'button' - toggleButton.setAttribute('aria-pressed', isActive ? 'true' : 'false') - toggleButton.setAttribute('data-key', dataKey) +function makeToggleButton(mode, isActive = false, classNames = '', dataKey = '') { + const toggleButton = document.createElement('button'); + toggleButton.className = classNames; + toggleButton.style.cssText = styles.toggleButton; + toggleButton.type = 'button'; + toggleButton.setAttribute('aria-pressed', isActive ? 'true' : 'false'); + toggleButton.setAttribute('data-key', dataKey); - const activeKey = isActive ? 'active' : 'inactive' + const activeKey = isActive ? 'active' : 'inactive'; - const toggleBg = document.createElement('div') - toggleBg.style.cssText = - styles.toggleButtonBg + styles[mode].toggleButtonBgState[activeKey] + const toggleBg = document.createElement('div'); + toggleBg.style.cssText = styles.toggleButtonBg + styles[mode].toggleButtonBgState[activeKey]; - const toggleKnob = document.createElement('div') - toggleKnob.style.cssText = - styles.toggleButtonKnob + styles.toggleButtonKnobState[activeKey] + const toggleKnob = document.createElement('div'); + toggleKnob.style.cssText = styles.toggleButtonKnob + styles.toggleButtonKnobState[activeKey]; - toggleButton.appendChild(toggleBg) - toggleButton.appendChild(toggleKnob) + toggleButton.appendChild(toggleBg); + toggleButton.appendChild(toggleKnob); - return toggleButton + return toggleButton; } /** @@ -1198,65 +1186,65 @@ function makeToggleButton (mode, isActive = false, classNames = '', dataKey = '' * Value to assign to the button's 'data-key' attribute. * @returns {HTMLDivElement} */ -function makeToggleButtonWithText (text, mode, isActive = false, toggleClassNames = '', textCssStyles = '', dataKey = '') { - const wrapper = document.createElement('div') - wrapper.style.cssText = styles.toggleButtonWrapper +function makeToggleButtonWithText(text, mode, isActive = false, toggleClassNames = '', textCssStyles = '', dataKey = '') { + const wrapper = document.createElement('div'); + wrapper.style.cssText = styles.toggleButtonWrapper; - const toggleButton = makeToggleButton(mode, isActive, toggleClassNames, dataKey) + const toggleButton = makeToggleButton(mode, isActive, toggleClassNames, dataKey); - const textDiv = document.createElement('div') - textDiv.style.cssText = styles.contentText + styles.toggleButtonText + styles[mode].toggleButtonText + textCssStyles - textDiv.textContent = text + const textDiv = document.createElement('div'); + textDiv.style.cssText = styles.contentText + styles.toggleButtonText + styles[mode].toggleButtonText + textCssStyles; + textDiv.textContent = text; - wrapper.appendChild(toggleButton) - wrapper.appendChild(textDiv) - return wrapper + wrapper.appendChild(toggleButton); + wrapper.appendChild(textDiv); + return wrapper; } /** * Create the default block symbol, for when the image isn't available. * @returns {HTMLDivElement} */ -function makeDefaultBlockIcon () { - const blockedIcon = document.createElement('div') - const dash = document.createElement('div') - blockedIcon.appendChild(dash) - blockedIcon.style.cssText = styles.circle - dash.style.cssText = styles.rectangle - return blockedIcon +function makeDefaultBlockIcon() { + const blockedIcon = document.createElement('div'); + const dash = document.createElement('div'); + blockedIcon.appendChild(dash); + blockedIcon.style.cssText = styles.circle; + dash.style.cssText = styles.rectangle; + return blockedIcon; } /** * Creates a share feedback link element. * @returns {HTMLAnchorElement} */ -function makeShareFeedbackLink () { - const feedbackLink = document.createElement('a') - feedbackLink.style.cssText = styles.feedbackLink - feedbackLink.target = '_blank' - feedbackLink.href = '#' - feedbackLink.text = sharedStrings.shareFeedback +function makeShareFeedbackLink() { + const feedbackLink = document.createElement('a'); + feedbackLink.style.cssText = styles.feedbackLink; + feedbackLink.target = '_blank'; + feedbackLink.href = '#'; + feedbackLink.text = sharedStrings.shareFeedback; // Open Feedback Form page through background event to avoid browser blocking extension link feedbackLink.addEventListener('click', function (e) { - e.preventDefault() - openShareFeedbackPage() - }) + e.preventDefault(); + openShareFeedbackPage(); + }); - return feedbackLink + return feedbackLink; } /** * Creates a share feedback link element, wrapped in a styled div. * @returns {HTMLDivElement} */ -function makeShareFeedbackRow () { - const feedbackRow = document.createElement('div') - feedbackRow.style.cssText = styles.feedbackRow +function makeShareFeedbackRow() { + const feedbackRow = document.createElement('div'); + feedbackRow.style.cssText = styles.feedbackRow; - const feedbackLink = makeShareFeedbackLink() - feedbackRow.appendChild(feedbackLink) + const feedbackLink = makeShareFeedbackLink(); + feedbackRow.appendChild(feedbackLink); - return feedbackRow + return feedbackRow; } /** @@ -1276,15 +1264,15 @@ function makeShareFeedbackRow () { * expected to do that. * @returns {{ container: HTMLDivElement, button: HTMLButtonElement }} */ -function makeLoginButton (buttonText, mode, hoverTextBody, icon, originalElement) { - const container = document.createElement('div') - container.style.cssText = 'position: relative;' - container.appendChild(makeFontFaceStyleElement()) +function makeLoginButton(buttonText, mode, hoverTextBody, icon, originalElement) { + const container = document.createElement('div'); + container.style.cssText = 'position: relative;'; + container.appendChild(makeFontFaceStyleElement()); - const shadowRoot = container.attachShadow({ mode: devMode ? 'open' : 'closed' }) + const shadowRoot = container.attachShadow({ mode: devMode ? 'open' : 'closed' }); // inherit any class styles on the button - container.className = 'fb-login-button FacebookLogin__button' - const { styleElement } = makeBaseStyleElement(mode) + container.className = 'fb-login-button FacebookLogin__button'; + const { styleElement } = makeBaseStyleElement(mode); styleElement.textContent += ` #DuckDuckGoPrivacyEssentialsHoverableText { display: none; @@ -1292,72 +1280,72 @@ function makeLoginButton (buttonText, mode, hoverTextBody, icon, originalElement #DuckDuckGoPrivacyEssentialsHoverable:hover #DuckDuckGoPrivacyEssentialsHoverableText { display: block; } - ` - shadowRoot.appendChild(styleElement) + `; + shadowRoot.appendChild(styleElement); - const hoverContainer = document.createElement('div') - hoverContainer.id = 'DuckDuckGoPrivacyEssentialsHoverable' - hoverContainer.style.cssText = styles.hoverContainer - shadowRoot.appendChild(hoverContainer) + const hoverContainer = document.createElement('div'); + hoverContainer.id = 'DuckDuckGoPrivacyEssentialsHoverable'; + hoverContainer.style.cssText = styles.hoverContainer; + shadowRoot.appendChild(hoverContainer); // Make the button - const button = makeButton(buttonText, mode) + const button = makeButton(buttonText, mode); // Add blocked icon if (!icon) { - button.appendChild(makeDefaultBlockIcon()) + button.appendChild(makeDefaultBlockIcon()); } else { - const imgElement = document.createElement('img') - imgElement.style.cssText = styles.loginIcon - imgElement.setAttribute('src', icon) - imgElement.setAttribute('height', '28px') - button.appendChild(imgElement) + const imgElement = document.createElement('img'); + imgElement.style.cssText = styles.loginIcon; + imgElement.setAttribute('src', icon); + imgElement.setAttribute('height', '28px'); + button.appendChild(imgElement); } - hoverContainer.appendChild(button) + hoverContainer.appendChild(button); // hover action - const hoverBox = document.createElement('div') - hoverBox.id = 'DuckDuckGoPrivacyEssentialsHoverableText' - hoverBox.style.cssText = styles.textBubble - const arrow = document.createElement('div') - arrow.style.cssText = styles.textArrow - hoverBox.appendChild(arrow) - const branding = createTitleRow('DuckDuckGo') - branding.style.cssText += styles.hoverTextTitle - hoverBox.appendChild(branding) - const hoverText = document.createElement('div') - hoverText.style.cssText = styles.hoverTextBody - hoverText.textContent = hoverTextBody + ' ' - hoverText.appendChild(getLearnMoreLink(mode)) - hoverBox.appendChild(hoverText) - - hoverContainer.appendChild(hoverBox) - const rect = originalElement.getBoundingClientRect() + const hoverBox = document.createElement('div'); + hoverBox.id = 'DuckDuckGoPrivacyEssentialsHoverableText'; + hoverBox.style.cssText = styles.textBubble; + const arrow = document.createElement('div'); + arrow.style.cssText = styles.textArrow; + hoverBox.appendChild(arrow); + const branding = createTitleRow('DuckDuckGo'); + branding.style.cssText += styles.hoverTextTitle; + hoverBox.appendChild(branding); + const hoverText = document.createElement('div'); + hoverText.style.cssText = styles.hoverTextBody; + hoverText.textContent = hoverTextBody + ' '; + hoverText.appendChild(getLearnMoreLink(mode)); + hoverBox.appendChild(hoverText); + + hoverContainer.appendChild(hoverBox); + const rect = originalElement.getBoundingClientRect(); // The left side of the hover popup may go offscreen if the // login button is all the way on the left side of the page. This // If that is the case, dynamically shift the box right so it shows // properly. if (rect.left < styles.textBubbleLeftShift) { - const leftShift = -rect.left + 10 // 10px away from edge of the screen - hoverBox.style.cssText += `left: ${leftShift}px;` - const change = (1 - (rect.left / styles.textBubbleLeftShift)) * (100 - styles.arrowDefaultLocationPercent) - arrow.style.cssText += `left: ${Math.max(10, styles.arrowDefaultLocationPercent - change)}%;` + const leftShift = -rect.left + 10; // 10px away from edge of the screen + hoverBox.style.cssText += `left: ${leftShift}px;`; + const change = (1 - rect.left / styles.textBubbleLeftShift) * (100 - styles.arrowDefaultLocationPercent); + arrow.style.cssText += `left: ${Math.max(10, styles.arrowDefaultLocationPercent - change)}%;`; } else if (rect.left + styles.textBubbleWidth - styles.textBubbleLeftShift > window.innerWidth) { - const rightShift = rect.left + styles.textBubbleWidth - styles.textBubbleLeftShift - const diff = Math.min(rightShift - window.innerWidth, styles.textBubbleLeftShift) - const rightMargin = 20 // Add some margin to the page, so scrollbar doesn't overlap. - hoverBox.style.cssText += `left: -${styles.textBubbleLeftShift + diff + rightMargin}px;` - const change = ((diff / styles.textBubbleLeftShift)) * (100 - styles.arrowDefaultLocationPercent) - arrow.style.cssText += `left: ${Math.max(10, styles.arrowDefaultLocationPercent + change)}%;` + const rightShift = rect.left + styles.textBubbleWidth - styles.textBubbleLeftShift; + const diff = Math.min(rightShift - window.innerWidth, styles.textBubbleLeftShift); + const rightMargin = 20; // Add some margin to the page, so scrollbar doesn't overlap. + hoverBox.style.cssText += `left: -${styles.textBubbleLeftShift + diff + rightMargin}px;`; + const change = (diff / styles.textBubbleLeftShift) * (100 - styles.arrowDefaultLocationPercent); + arrow.style.cssText += `left: ${Math.max(10, styles.arrowDefaultLocationPercent + change)}%;`; } else { - hoverBox.style.cssText += `left: -${styles.textBubbleLeftShift}px;` - arrow.style.cssText += `left: ${styles.arrowDefaultLocationPercent}%;` + hoverBox.style.cssText += `left: -${styles.textBubbleLeftShift}px;`; + arrow.style.cssText += `left: ${styles.arrowDefaultLocationPercent}%;`; } return { button, - container - } + container, + }; } /** @@ -1372,83 +1360,83 @@ function makeLoginButton (buttonText, mode, hoverTextBody, icon, originalElement * The parameters passed to acceptFunction when it is called. * TODO: Have the caller bind these arguments to the function instead. */ -function makeModal (entity, acceptFunction, ...acceptFunctionParams) { - const icon = entityData[entity].modalIcon +function makeModal(entity, acceptFunction, ...acceptFunctionParams) { + const icon = entityData[entity].modalIcon; - const modalContainer = document.createElement('div') - modalContainer.setAttribute('data-key', 'modal') - modalContainer.style.cssText = styles.modalContainer + const modalContainer = document.createElement('div'); + modalContainer.setAttribute('data-key', 'modal'); + modalContainer.style.cssText = styles.modalContainer; - modalContainer.appendChild(makeFontFaceStyleElement()) + modalContainer.appendChild(makeFontFaceStyleElement()); const closeModal = () => { - document.body.removeChild(modalContainer) - abortSurrogateConfirmation(entity) - } + document.body.removeChild(modalContainer); + abortSurrogateConfirmation(entity); + }; // Protect the contents of our modal inside a shadowRoot, to avoid // it being styled by the website's stylesheets. - const shadowRoot = modalContainer.attachShadow({ mode: devMode ? 'open' : 'closed' }) - const { styleElement } = makeBaseStyleElement('lightMode') - shadowRoot.appendChild(styleElement) + const shadowRoot = modalContainer.attachShadow({ mode: devMode ? 'open' : 'closed' }); + const { styleElement } = makeBaseStyleElement('lightMode'); + shadowRoot.appendChild(styleElement); - const pageOverlay = document.createElement('div') - pageOverlay.style.cssText = styles.overlay + const pageOverlay = document.createElement('div'); + pageOverlay.style.cssText = styles.overlay; - const modal = document.createElement('div') - modal.style.cssText = styles.modal + const modal = document.createElement('div'); + modal.style.cssText = styles.modal; // Title - const modalTitle = createTitleRow('DuckDuckGo', null, closeModal) - modal.appendChild(modalTitle) + const modalTitle = createTitleRow('DuckDuckGo', null, closeModal); + modal.appendChild(modalTitle); - const iconElement = document.createElement('img') - iconElement.style.cssText = styles.icon + styles.modalIcon - iconElement.setAttribute('src', icon) - iconElement.setAttribute('height', '70px') + const iconElement = document.createElement('img'); + iconElement.style.cssText = styles.icon + styles.modalIcon; + iconElement.setAttribute('src', icon); + iconElement.setAttribute('height', '70px'); - const title = document.createElement('div') - title.style.cssText = styles.modalContentTitle - title.textContent = entityData[entity].modalTitle + const title = document.createElement('div'); + title.style.cssText = styles.modalContentTitle; + title.textContent = entityData[entity].modalTitle; // Content - const modalContent = document.createElement('div') - modalContent.style.cssText = styles.modalContent + const modalContent = document.createElement('div'); + modalContent.style.cssText = styles.modalContent; - const message = document.createElement('div') - message.style.cssText = styles.modalContentText - message.textContent = entityData[entity].modalText + ' ' - message.appendChild(getLearnMoreLink()) + const message = document.createElement('div'); + message.style.cssText = styles.modalContentText; + message.textContent = entityData[entity].modalText + ' '; + message.appendChild(getLearnMoreLink()); - modalContent.appendChild(iconElement) - modalContent.appendChild(title) - modalContent.appendChild(message) + modalContent.appendChild(iconElement); + modalContent.appendChild(title); + modalContent.appendChild(message); // Buttons - const buttonRow = document.createElement('div') - buttonRow.style.cssText = styles.modalButtonRow - const allowButton = makeButton(entityData[entity].modalAcceptText, 'lightMode') - allowButton.style.cssText += styles.modalButton + 'margin-bottom: 8px;' - allowButton.setAttribute('data-key', 'allow') - allowButton.addEventListener('click', function doLogin () { - acceptFunction(...acceptFunctionParams) - document.body.removeChild(modalContainer) - }) - const rejectButton = makeButton(entityData[entity].modalRejectText, 'cancelMode') - rejectButton.setAttribute('data-key', 'reject') - rejectButton.style.cssText += styles.modalButton - rejectButton.addEventListener('click', closeModal) - - buttonRow.appendChild(allowButton) - buttonRow.appendChild(rejectButton) - modalContent.appendChild(buttonRow) - - modal.appendChild(modalContent) - - shadowRoot.appendChild(pageOverlay) - shadowRoot.appendChild(modal) - - document.body.insertBefore(modalContainer, document.body.childNodes[0]) + const buttonRow = document.createElement('div'); + buttonRow.style.cssText = styles.modalButtonRow; + const allowButton = makeButton(entityData[entity].modalAcceptText, 'lightMode'); + allowButton.style.cssText += styles.modalButton + 'margin-bottom: 8px;'; + allowButton.setAttribute('data-key', 'allow'); + allowButton.addEventListener('click', function doLogin() { + acceptFunction(...acceptFunctionParams); + document.body.removeChild(modalContainer); + }); + const rejectButton = makeButton(entityData[entity].modalRejectText, 'cancelMode'); + rejectButton.setAttribute('data-key', 'reject'); + rejectButton.style.cssText += styles.modalButton; + rejectButton.addEventListener('click', closeModal); + + buttonRow.appendChild(allowButton); + buttonRow.appendChild(rejectButton); + modalContent.appendChild(buttonRow); + + modal.appendChild(modalContent); + + shadowRoot.appendChild(pageOverlay); + shadowRoot.appendChild(modal); + + document.body.insertBefore(modalContainer, document.body.childNodes[0]); } /** @@ -1461,48 +1449,48 @@ function makeModal (entity, acceptFunction, ...acceptFunctionParams) { * If provided, a close button is added that calls this function when clicked. * @returns {HTMLDivElement} */ -function createTitleRow (message, textButton, closeBtnFn) { +function createTitleRow(message, textButton, closeBtnFn) { // Create row container - const row = document.createElement('div') - row.style.cssText = styles.titleBox + const row = document.createElement('div'); + row.style.cssText = styles.titleBox; // Logo - const logoContainer = document.createElement('div') - logoContainer.style.cssText = styles.logo - const logoElement = document.createElement('img') - logoElement.setAttribute('src', logoImg) - logoElement.setAttribute('height', '21px') - logoElement.style.cssText = styles.logoImg - logoContainer.appendChild(logoElement) - row.appendChild(logoContainer) + const logoContainer = document.createElement('div'); + logoContainer.style.cssText = styles.logo; + const logoElement = document.createElement('img'); + logoElement.setAttribute('src', logoImg); + logoElement.setAttribute('height', '21px'); + logoElement.style.cssText = styles.logoImg; + logoContainer.appendChild(logoElement); + row.appendChild(logoContainer); // Content box title - const msgElement = document.createElement('div') - msgElement.id = titleID // Ensure we can find this to potentially hide it later. - msgElement.textContent = message - msgElement.style.cssText = styles.title - row.appendChild(msgElement) + const msgElement = document.createElement('div'); + msgElement.id = titleID; // Ensure we can find this to potentially hide it later. + msgElement.textContent = message; + msgElement.style.cssText = styles.title; + row.appendChild(msgElement); // Close Button if (typeof closeBtnFn === 'function') { - const closeButton = document.createElement('button') - closeButton.style.cssText = styles.closeButton - const closeIconImg = document.createElement('img') - closeIconImg.setAttribute('src', closeIcon) - closeIconImg.setAttribute('height', '12px') - closeIconImg.style.cssText = styles.closeIcon - closeButton.appendChild(closeIconImg) - closeButton.addEventListener('click', closeBtnFn) - row.appendChild(closeButton) + const closeButton = document.createElement('button'); + closeButton.style.cssText = styles.closeButton; + const closeIconImg = document.createElement('img'); + closeIconImg.setAttribute('src', closeIcon); + closeIconImg.setAttribute('height', '12px'); + closeIconImg.style.cssText = styles.closeIcon; + closeButton.appendChild(closeIconImg); + closeButton.addEventListener('click', closeBtnFn); + row.appendChild(closeButton); } // Text button for very small boxes if (textButton) { - textButton.id = titleID + 'TextButton' - row.appendChild(textButton) + textButton.id = titleID + 'TextButton'; + row.appendChild(textButton); } - return row + return row; } /** @@ -1521,82 +1509,82 @@ function createTitleRow (message, textButton, closeBtnFn) { * Bottom row to append to the placeholder, if any. * @returns {{ contentBlock: HTMLDivElement, shadowRoot: ShadowRoot }} */ -function createContentBlock (widget, button, textButton, img, bottomRow) { - const contentBlock = document.createElement('div') - contentBlock.style.cssText = styles.wrapperDiv +function createContentBlock(widget, button, textButton, img, bottomRow) { + const contentBlock = document.createElement('div'); + contentBlock.style.cssText = styles.wrapperDiv; - contentBlock.appendChild(makeFontFaceStyleElement()) + contentBlock.appendChild(makeFontFaceStyleElement()); // Put everything else inside the shadowRoot of the wrapper element to // reduce the chances of the website's stylesheets messing up the // placeholder's appearance. - const shadowRootMode = devMode ? 'open' : 'closed' - const shadowRoot = contentBlock.attachShadow({ mode: shadowRootMode }) + const shadowRootMode = devMode ? 'open' : 'closed'; + const shadowRoot = contentBlock.attachShadow({ mode: shadowRootMode }); // Style element includes our font & overwrites page styles - const { wrapperClass, styleElement } = makeBaseStyleElement(widget.getMode()) - shadowRoot.appendChild(styleElement) + const { wrapperClass, styleElement } = makeBaseStyleElement(widget.getMode()); + shadowRoot.appendChild(styleElement); // Create overall grid structure - const element = document.createElement('div') - element.style.cssText = styles.block + styles[widget.getMode()].background + styles[widget.getMode()].textFont + const element = document.createElement('div'); + element.style.cssText = styles.block + styles[widget.getMode()].background + styles[widget.getMode()].textFont; if (widget.replaceSettings.type === 'youtube-video') { - element.style.cssText += styles.youTubeDialogBlock + element.style.cssText += styles.youTubeDialogBlock; } - element.className = wrapperClass - shadowRoot.appendChild(element) + element.className = wrapperClass; + shadowRoot.appendChild(element); // grid of three rows - const titleRow = document.createElement('div') - titleRow.style.cssText = styles.headerRow - element.appendChild(titleRow) - titleRow.appendChild(createTitleRow('DuckDuckGo', textButton)) + const titleRow = document.createElement('div'); + titleRow.style.cssText = styles.headerRow; + element.appendChild(titleRow); + titleRow.appendChild(createTitleRow('DuckDuckGo', textButton)); - const contentRow = document.createElement('div') - contentRow.style.cssText = styles.content + const contentRow = document.createElement('div'); + contentRow.style.cssText = styles.content; if (img) { - const imageRow = document.createElement('div') - imageRow.style.cssText = styles.imgRow - const imgElement = document.createElement('img') - imgElement.style.cssText = styles.icon - imgElement.setAttribute('src', img) - imgElement.setAttribute('height', '70px') - imageRow.appendChild(imgElement) - element.appendChild(imageRow) + const imageRow = document.createElement('div'); + imageRow.style.cssText = styles.imgRow; + const imgElement = document.createElement('img'); + imgElement.style.cssText = styles.icon; + imgElement.setAttribute('src', img); + imgElement.setAttribute('height', '70px'); + imageRow.appendChild(imgElement); + element.appendChild(imageRow); } - const contentTitle = document.createElement('div') - contentTitle.style.cssText = styles.contentTitle - contentTitle.textContent = widget.replaceSettings.infoTitle - contentTitle.id = 'contentTitle' - contentRow.appendChild(contentTitle) - const contentText = document.createElement('div') - contentText.style.cssText = styles.contentText - const contentTextSpan = document.createElement('span') - contentTextSpan.id = 'infoText' - contentTextSpan.textContent = widget.replaceSettings.infoText + ' ' - contentText.appendChild(contentTextSpan) - contentText.appendChild(getLearnMoreLink()) - contentRow.appendChild(contentText) - element.appendChild(contentRow) - - const buttonRow = document.createElement('div') - buttonRow.style.cssText = styles.buttonRow - buttonRow.appendChild(button) - contentText.appendChild(buttonRow) + const contentTitle = document.createElement('div'); + contentTitle.style.cssText = styles.contentTitle; + contentTitle.textContent = widget.replaceSettings.infoTitle; + contentTitle.id = 'contentTitle'; + contentRow.appendChild(contentTitle); + const contentText = document.createElement('div'); + contentText.style.cssText = styles.contentText; + const contentTextSpan = document.createElement('span'); + contentTextSpan.id = 'infoText'; + contentTextSpan.textContent = widget.replaceSettings.infoText + ' '; + contentText.appendChild(contentTextSpan); + contentText.appendChild(getLearnMoreLink()); + contentRow.appendChild(contentText); + element.appendChild(contentRow); + + const buttonRow = document.createElement('div'); + buttonRow.style.cssText = styles.buttonRow; + buttonRow.appendChild(button); + contentText.appendChild(buttonRow); if (bottomRow) { - contentRow.appendChild(bottomRow) + contentRow.appendChild(bottomRow); } /** Share Feedback Link */ if (widget.replaceSettings.type === 'youtube-video') { - const feedbackRow = makeShareFeedbackRow() - shadowRoot.appendChild(feedbackRow) + const feedbackRow = makeShareFeedbackRow(); + shadowRoot.appendChild(feedbackRow); } - return { contentBlock, shadowRoot } + return { contentBlock, shadowRoot }; } /** @@ -1605,39 +1593,36 @@ function createContentBlock (widget, button, textButton, img, bottomRow) { * @param {DuckWidget} widget * @returns {{ blockingDialog: HTMLElement, shadowRoot: ShadowRoot }} */ -function createYouTubeBlockingDialog (trackingElement, widget) { - const button = makeButton(widget.replaceSettings.buttonText, widget.getMode()) - const textButton = makeTextButton(widget.replaceSettings.buttonText, widget.getMode()) +function createYouTubeBlockingDialog(trackingElement, widget) { + const button = makeButton(widget.replaceSettings.buttonText, widget.getMode()); + const textButton = makeTextButton(widget.replaceSettings.buttonText, widget.getMode()); - const bottomRow = document.createElement('div') - bottomRow.style.cssText = styles.youTubeDialogBottomRow + const bottomRow = document.createElement('div'); + bottomRow.style.cssText = styles.youTubeDialogBottomRow; const previewToggle = makeToggleButtonWithText( widget.replaceSettings.previewToggleText, widget.getMode(), false, '', '', - 'yt-preview-toggle' - ) - previewToggle.addEventListener( - 'click', - () => makeModal(widget.entity, () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: true }), widget.entity) - ) - bottomRow.appendChild(previewToggle) - - const { contentBlock, shadowRoot } = createContentBlock( - widget, button, textButton, null, bottomRow - ) - contentBlock.id = trackingElement.id - contentBlock.style.cssText += styles.wrapperDiv + styles.youTubeWrapperDiv - - button.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)) - textButton.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)) + 'yt-preview-toggle', + ); + previewToggle.addEventListener('click', () => + makeModal(widget.entity, () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: true }), widget.entity), + ); + bottomRow.appendChild(previewToggle); + + const { contentBlock, shadowRoot } = createContentBlock(widget, button, textButton, null, bottomRow); + contentBlock.id = trackingElement.id; + contentBlock.style.cssText += styles.wrapperDiv + styles.youTubeWrapperDiv; + + button.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)); + textButton.addEventListener('click', widget.clickFunction(trackingElement, contentBlock)); return { blockingDialog: contentBlock, - shadowRoot - } + shadowRoot, + }; } /** @@ -1651,82 +1636,76 @@ function createYouTubeBlockingDialog (trackingElement, widget) { * @returns {{ youTubePreview: HTMLElement, shadowRoot: ShadowRoot }} * Object containing the YouTube Preview element and its shadowRoot. */ -function createYouTubePreview (originalElement, widget) { - const youTubePreview = document.createElement('div') - youTubePreview.id = originalElement.id - youTubePreview.style.cssText = styles.wrapperDiv + styles.placeholderWrapperDiv +function createYouTubePreview(originalElement, widget) { + const youTubePreview = document.createElement('div'); + youTubePreview.id = originalElement.id; + youTubePreview.style.cssText = styles.wrapperDiv + styles.placeholderWrapperDiv; - youTubePreview.appendChild(makeFontFaceStyleElement()) + youTubePreview.appendChild(makeFontFaceStyleElement()); // Protect the contents of our placeholder inside a shadowRoot, to avoid // it being styled by the website's stylesheets. - const shadowRoot = youTubePreview.attachShadow({ mode: devMode ? 'open' : 'closed' }) - const { wrapperClass, styleElement } = makeBaseStyleElement(widget.getMode()) - shadowRoot.appendChild(styleElement) + const shadowRoot = youTubePreview.attachShadow({ mode: devMode ? 'open' : 'closed' }); + const { wrapperClass, styleElement } = makeBaseStyleElement(widget.getMode()); + shadowRoot.appendChild(styleElement); - const youTubePreviewDiv = document.createElement('div') - youTubePreviewDiv.style.cssText = styles.youTubeDialogDiv - youTubePreviewDiv.classList.add(wrapperClass) - shadowRoot.appendChild(youTubePreviewDiv) + const youTubePreviewDiv = document.createElement('div'); + youTubePreviewDiv.style.cssText = styles.youTubeDialogDiv; + youTubePreviewDiv.classList.add(wrapperClass); + shadowRoot.appendChild(youTubePreviewDiv); /** Preview Image */ - const previewImageWrapper = document.createElement('div') - previewImageWrapper.style.cssText = styles.youTubePreviewWrapperImg - youTubePreviewDiv.appendChild(previewImageWrapper) + const previewImageWrapper = document.createElement('div'); + previewImageWrapper.style.cssText = styles.youTubePreviewWrapperImg; + youTubePreviewDiv.appendChild(previewImageWrapper); // We use an image element for the preview image so that we can ensure // the referrer isn't passed. - const previewImageElement = document.createElement('img') - previewImageElement.setAttribute('referrerPolicy', 'no-referrer') - previewImageElement.style.cssText = styles.youTubePreviewImg - previewImageWrapper.appendChild(previewImageElement) + const previewImageElement = document.createElement('img'); + previewImageElement.setAttribute('referrerPolicy', 'no-referrer'); + previewImageElement.style.cssText = styles.youTubePreviewImg; + previewImageWrapper.appendChild(previewImageElement); - const innerDiv = document.createElement('div') - innerDiv.style.cssText = styles.youTubePlaceholder + const innerDiv = document.createElement('div'); + innerDiv.style.cssText = styles.youTubePlaceholder; /** Top section */ - const topSection = document.createElement('div') - topSection.style.cssText = styles.youTubeTopSection - innerDiv.appendChild(topSection) + const topSection = document.createElement('div'); + topSection.style.cssText = styles.youTubeTopSection; + innerDiv.appendChild(topSection); /** Video Title */ - const titleElement = document.createElement('p') - titleElement.style.cssText = styles.youTubeTitle - topSection.appendChild(titleElement) + const titleElement = document.createElement('p'); + titleElement.style.cssText = styles.youTubeTitle; + topSection.appendChild(titleElement); /** Text Button on top section */ // Use darkMode styles because the preview background is dark and causes poor contrast // with lightMode button, making it hard to read. - const textButton = makeTextButton(widget.replaceSettings.buttonText, 'darkMode') - textButton.id = titleID + 'TextButton' + const textButton = makeTextButton(widget.replaceSettings.buttonText, 'darkMode'); + textButton.id = titleID + 'TextButton'; - textButton.addEventListener( - 'click', - widget.clickFunction(originalElement, youTubePreview) - ) - topSection.appendChild(textButton) + textButton.addEventListener('click', widget.clickFunction(originalElement, youTubePreview)); + topSection.appendChild(textButton); /** Play Button */ - const playButtonRow = document.createElement('div') - playButtonRow.style.cssText = styles.youTubePlayButtonRow + const playButtonRow = document.createElement('div'); + playButtonRow.style.cssText = styles.youTubePlayButtonRow; - const playButton = makeButton('', widget.getMode()) - playButton.style.cssText += styles.youTubePlayButton + const playButton = makeButton('', widget.getMode()); + playButton.style.cssText += styles.youTubePlayButton; - const videoPlayImg = document.createElement('img') - const videoPlayIcon = widget.replaceSettings.placeholder.videoPlayIcon[widget.getMode()] - videoPlayImg.setAttribute('src', videoPlayIcon) - playButton.appendChild(videoPlayImg) + const videoPlayImg = document.createElement('img'); + const videoPlayIcon = widget.replaceSettings.placeholder.videoPlayIcon[widget.getMode()]; + videoPlayImg.setAttribute('src', videoPlayIcon); + playButton.appendChild(videoPlayImg); - playButton.addEventListener( - 'click', - widget.clickFunction(originalElement, youTubePreview) - ) - playButtonRow.appendChild(playButton) - innerDiv.appendChild(playButtonRow) + playButton.addEventListener('click', widget.clickFunction(originalElement, youTubePreview)); + playButtonRow.appendChild(playButton); + innerDiv.appendChild(playButtonRow); /** Preview Toggle */ - const previewToggleRow = document.createElement('div') - previewToggleRow.style.cssText = styles.youTubePreviewToggleRow + const previewToggleRow = document.createElement('div'); + previewToggleRow.style.cssText = styles.youTubePreviewToggleRow; // TODO: Use `widget.replaceSettings.placeholder.previewToggleEnabledDuckDuckGoText` for toggle // copy when implementing mobile YT CTL Preview @@ -1736,16 +1715,13 @@ function createYouTubePreview (originalElement, widget) { true, '', styles.youTubePreviewToggleText, - 'yt-preview-toggle' - ) - previewToggle.addEventListener( - 'click', - () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: false }) - ) + 'yt-preview-toggle', + ); + previewToggle.addEventListener('click', () => ctl.messaging.notify('setYoutubePreviewsEnabled', { youtubePreviewsEnabled: false })); /** Preview Info Text */ - const previewText = document.createElement('div') - previewText.style.cssText = styles.contentText + styles.toggleButtonText + styles.youTubePreviewInfoText + const previewText = document.createElement('div'); + previewText.style.cssText = styles.contentText + styles.toggleButtonText + styles.youTubePreviewInfoText; // Since this string contains an anchor element, setting innerText won't // work. // Warning: This is not ideal! The translated (and original) strings must be @@ -1753,44 +1729,45 @@ function createYouTubePreview (originalElement, widget) { // Ideally, the translation system would allow only certain element // types to be included, and would avoid the URLs for links being // included in the translations. - previewText.insertAdjacentHTML( - 'beforeend', widget.replaceSettings.placeholder.previewInfoText - ) - const previewTextLink = previewText.querySelector('a') + previewText.insertAdjacentHTML('beforeend', widget.replaceSettings.placeholder.previewInfoText); + const previewTextLink = previewText.querySelector('a'); if (previewTextLink) { - const newPreviewTextLink = getLearnMoreLink(widget.getMode()) - newPreviewTextLink.innerText = previewTextLink.innerText - previewTextLink.replaceWith(newPreviewTextLink) + const newPreviewTextLink = getLearnMoreLink(widget.getMode()); + newPreviewTextLink.innerText = previewTextLink.innerText; + previewTextLink.replaceWith(newPreviewTextLink); } - previewToggleRow.appendChild(previewToggle) - previewToggleRow.appendChild(previewText) - innerDiv.appendChild(previewToggleRow) + previewToggleRow.appendChild(previewToggle); + previewToggleRow.appendChild(previewText); + innerDiv.appendChild(previewToggleRow); - youTubePreviewDiv.appendChild(innerDiv) + youTubePreviewDiv.appendChild(innerDiv); // We use .then() instead of await here to show the placeholder right away // while the YouTube endpoint takes it time to respond. - const videoURL = originalElement.src || originalElement.getAttribute('data-src') - ctl.messaging.request('getYouTubeVideoDetails', { videoURL }) + const videoURL = originalElement.src || originalElement.getAttribute('data-src'); + ctl.messaging + .request('getYouTubeVideoDetails', { videoURL }) // eslint-disable-next-line promise/prefer-await-to-then .then(({ videoURL: videoURLResp, status, title, previewImage }) => { - if (!status || videoURLResp !== videoURL) { return } + if (!status || videoURLResp !== videoURL) { + return; + } if (status === 'success') { - titleElement.innerText = title - titleElement.title = title + titleElement.innerText = title; + titleElement.title = title; if (previewImage) { - previewImageElement.setAttribute('src', previewImage) + previewImageElement.setAttribute('src', previewImage); } - widget.autoplay = true + widget.autoplay = true; } - }) + }); /** Share Feedback Link */ - const feedbackRow = makeShareFeedbackRow() - shadowRoot.appendChild(feedbackRow) + const feedbackRow = makeShareFeedbackRow(); + shadowRoot.appendChild(feedbackRow); - return { youTubePreview, shadowRoot } + return { youTubePreview, shadowRoot }; } /** @@ -1799,133 +1776,129 @@ function createYouTubePreview (originalElement, widget) { export default class ClickToLoad extends ContentFeature { /** @type {MessagingContext} */ - #messagingContext + #messagingContext; - async init (args) { + async init(args) { /** * Bail if no messaging backend - this is a debugging feature to ensure we don't * accidentally enabled this */ if (!this.messaging) { - throw new Error('Cannot operate click to load without a messaging backend') + throw new Error('Cannot operate click to load without a messaging backend'); } - _messagingModuleScope = this.messaging - _addDebugFlag = this.addDebugFlag.bind(this) - - const websiteOwner = args?.site?.parentEntity - const settings = args?.featureSettings?.clickToLoad || {} - const locale = args?.locale || 'en' - const localizedConfig = getConfig(locale) - config = localizedConfig.config - sharedStrings = localizedConfig.sharedStrings + _messagingModuleScope = this.messaging; + _addDebugFlag = this.addDebugFlag.bind(this); + + const websiteOwner = args?.site?.parentEntity; + const settings = args?.featureSettings?.clickToLoad || {}; + const locale = args?.locale || 'en'; + const localizedConfig = getConfig(locale); + config = localizedConfig.config; + sharedStrings = localizedConfig.sharedStrings; // update styles if asset config was sent - styles = getStyles(this.assetConfig) + styles = getStyles(this.assetConfig); /** * Register Custom Elements only when Click to Load is initialized, to ensure it is only * called when config is ready and any previous context have been appropriately invalidated * prior when applicable (ie Firefox when hot reloading the Extension) */ - registerCustomElements() + registerCustomElements(); for (const entity of Object.keys(config)) { // Strip config entities that are first-party, or aren't enabled in the // extension's clickToLoad settings. - if ((websiteOwner && entity === websiteOwner) || - !settings[entity] || - settings[entity].state !== 'enabled') { - delete config[entity] - continue + if ((websiteOwner && entity === websiteOwner) || !settings[entity] || settings[entity].state !== 'enabled') { + delete config[entity]; + continue; } // Populate the entities and entityData data structures. // TODO: Remove them and this logic, they seem unnecessary. - entities.push(entity) + entities.push(entity); - const shouldShowLoginModal = !!config[entity].informationalModal - const currentEntityData = { shouldShowLoginModal } + const shouldShowLoginModal = !!config[entity].informationalModal; + const currentEntityData = { shouldShowLoginModal }; if (shouldShowLoginModal) { - const { informationalModal } = config[entity] - currentEntityData.modalIcon = informationalModal.icon - currentEntityData.modalTitle = informationalModal.messageTitle - currentEntityData.modalText = informationalModal.messageBody - currentEntityData.modalAcceptText = informationalModal.confirmButtonText - currentEntityData.modalRejectText = informationalModal.rejectButtonText + const { informationalModal } = config[entity]; + currentEntityData.modalIcon = informationalModal.icon; + currentEntityData.modalTitle = informationalModal.messageTitle; + currentEntityData.modalText = informationalModal.messageBody; + currentEntityData.modalAcceptText = informationalModal.confirmButtonText; + currentEntityData.modalRejectText = informationalModal.rejectButtonText; } - entityData[entity] = currentEntityData + entityData[entity] = currentEntityData; } // Listen for window events from "surrogate" scripts. window.addEventListener('ddg-ctp', (/** @type {CustomEvent} */ event) => { - if (!('detail' in event)) return + if (!('detail' in event)) return; - const entity = event.detail?.entity + const entity = event.detail?.entity; if (!entities.includes(entity)) { // Unknown entity, reject - return + return; } if (event.detail?.appID) { - appID = JSON.stringify(event.detail.appID).replace(/"/g, '') + appID = JSON.stringify(event.detail.appID).replace(/"/g, ''); } // Handle login call if (event.detail?.action === 'login') { // Even if the user cancels the login attempt, consider Facebook Click to // Load to have been active on the page if the user reports the page as broken. if (entity === 'Facebook, Inc.') { - notifyFacebookLogin() + notifyFacebookLogin(); } if (entityData[entity].shouldShowLoginModal) { - handleUnblockConfirmation(this.platform.name, entity, runLogin, entity) + handleUnblockConfirmation(this.platform.name, entity, runLogin, entity); } else { - runLogin(entity) + runLogin(entity); } } - }) + }); // Listen to message from Platform letting CTL know that we're ready to // replace elements in the page - + this.messaging.subscribe( 'displayClickToLoadPlaceholders', // TODO: Pass `message.options.ruleAction` through, that way only // content corresponding to the entity for that ruleAction need to // be replaced with a placeholder. - () => this.replaceClickToLoadElements() - ) + () => this.replaceClickToLoadElements(), + ); // Request the current state of Click to Load from the platform. // Note: When the response is received, the response handler resolves // the readyToDisplayPlaceholders Promise. - const clickToLoadState = await this.messaging.request('getClickToLoadState') - this.onClickToLoadState(clickToLoadState) + const clickToLoadState = await this.messaging.request('getClickToLoadState'); + this.onClickToLoadState(clickToLoadState); // Then wait for the page to finish loading, and resolve the // afterPageLoad Promise. if (document.readyState === 'complete') { - afterPageLoadResolver() + afterPageLoadResolver(); } else { - window.addEventListener('load', afterPageLoadResolver, { once: true }) + window.addEventListener('load', afterPageLoadResolver, { once: true }); } - await afterPageLoad + await afterPageLoad; // On some websites, the "ddg-ctp-ready" event is occasionally // dispatched too early, before the listener is ready to receive it. // To counter that, catch "ddg-ctp-surrogate-load" events dispatched // _after_ page, so the "ddg-ctp-ready" event can be dispatched again. - window.addEventListener( - 'ddg-ctp-surrogate-load', () => { - originalWindowDispatchEvent(createCustomEvent('ddg-ctp-ready')) - } - ) + window.addEventListener('ddg-ctp-surrogate-load', () => { + originalWindowDispatchEvent(createCustomEvent('ddg-ctp-ready')); + }); // Then wait for any in-progress element replacements, before letting // the surrogate scripts know to start. window.setTimeout(() => { - originalWindowDispatchEvent(createCustomEvent('ddg-ctp-ready')) - }, 0) + originalWindowDispatchEvent(createCustomEvent('ddg-ctp-ready')); + }, 0); } /** @@ -1934,21 +1907,21 @@ export default class ClickToLoad extends ContentFeature { * SendMessageMessagingTransport that wraps this communication. * This can be removed once they have their own Messaging integration. */ - update (message) { + update(message) { // TODO: Once all Click to Load messages include the feature property, drop // messages that don't include the feature property too. - if (message?.feature && message?.feature !== 'clickToLoad') return + if (message?.feature && message?.feature !== 'clickToLoad') return; - const messageType = message?.messageType - if (!messageType) return + const messageType = message?.messageType; + if (!messageType) return; if (!this._clickToLoadMessagingTransport) { - throw new Error('_clickToLoadMessagingTransport not ready. Cannot operate click to load without a messaging backend') + throw new Error('_clickToLoadMessagingTransport not ready. Cannot operate click to load without a messaging backend'); } // Send to Messaging layer the response or subscription message received // from the Platform. - return this._clickToLoadMessagingTransport.onResponse(message) + return this._clickToLoadMessagingTransport.onResponse(message); } /** @@ -1957,13 +1930,13 @@ export default class ClickToLoad extends ContentFeature { * @param {boolean} state.devMode Developer or Production environment * @param {boolean} state.youtubePreviewsEnabled YouTube Click to Load - YT Previews enabled flag */ - onClickToLoadState (state) { - devMode = state.devMode - isYoutubePreviewsEnabled = state.youtubePreviewsEnabled + onClickToLoadState(state) { + devMode = state.devMode; + isYoutubePreviewsEnabled = state.youtubePreviewsEnabled; // Mark the feature as ready, to allow placeholder // replacements to start. - readyToDisplayPlaceholdersResolver() + readyToDisplayPlaceholdersResolver(); } /** @@ -1973,32 +1946,34 @@ export default class ClickToLoad extends ContentFeature { * one of the expected CSS selectors). If omitted, all matching elements * in the document will be replaced instead. */ - async replaceClickToLoadElements (targetElement) { - await readyToDisplayPlaceholders + async replaceClickToLoadElements(targetElement) { + await readyToDisplayPlaceholders; for (const entity of Object.keys(config)) { for (const widgetData of Object.values(config[entity].elementData)) { - const selector = widgetData.selectors.join() + const selector = widgetData.selectors.join(); - let trackingElements = [] + let trackingElements = []; if (targetElement) { if (targetElement.matches(selector)) { - trackingElements.push(targetElement) + trackingElements.push(targetElement); } } else { - trackingElements = Array.from(document.querySelectorAll(selector)) + trackingElements = Array.from(document.querySelectorAll(selector)); } - await Promise.all(trackingElements.map(trackingElement => { - if (knownTrackingElements.has(trackingElement)) { - return Promise.resolve() - } + await Promise.all( + trackingElements.map((trackingElement) => { + if (knownTrackingElements.has(trackingElement)) { + return Promise.resolve(); + } - knownTrackingElements.add(trackingElement) + knownTrackingElements.add(trackingElement); - const widget = new DuckWidget(widgetData, trackingElement, entity, this.platform) - return createPlaceholderElementAndReplace(widget, trackingElement) - })) + const widget = new DuckWidget(widgetData, trackingElement, entity, this.platform); + return createPlaceholderElementAndReplace(widget, trackingElement); + }), + ); } } } @@ -2006,31 +1981,31 @@ export default class ClickToLoad extends ContentFeature { /** * @returns {MessagingContext} */ - get messagingContext () { - if (this.#messagingContext) return this.#messagingContext - this.#messagingContext = this._createMessagingContext() - return this.#messagingContext + get messagingContext() { + if (this.#messagingContext) return this.#messagingContext; + this.#messagingContext = this._createMessagingContext(); + return this.#messagingContext; } // Messaging layer between Click to Load and the Platform - get messaging () { - if (this._messaging) return this._messaging + get messaging() { + if (this._messaging) return this._messaging; if (this.platform.name === 'android' || this.platform.name === 'extension') { - this._clickToLoadMessagingTransport = new SendMessageMessagingTransport() - const config = new TestTransportConfig(this._clickToLoadMessagingTransport) - this._messaging = new Messaging(this.messagingContext, config) - return this._messaging + this._clickToLoadMessagingTransport = new SendMessageMessagingTransport(); + const config = new TestTransportConfig(this._clickToLoadMessagingTransport); + this._messaging = new Messaging(this.messagingContext, config); + return this._messaging; } else if (this.platform.name === 'ios' || this.platform.name === 'macos') { const config = new WebkitMessagingConfig({ secret: '', hasModernWebkitAPI: true, - webkitMessageHandlerNames: ['contentScopeScriptsIsolated'] - }) - this._messaging = new Messaging(this.messagingContext, config) - return this._messaging + webkitMessageHandlerNames: ['contentScopeScriptsIsolated'], + }); + this._messaging = new Messaging(this.messagingContext, config); + return this._messaging; } else { - throw new Error('Messaging not supported yet on platform: ' + this.name) + throw new Error('Messaging not supported yet on platform: ' + this.name); } } } diff --git a/injected/src/features/click-to-load/components/ctl-login-button.js b/injected/src/features/click-to-load/components/ctl-login-button.js index 21d0da496..b1ac52c7b 100644 --- a/injected/src/features/click-to-load/components/ctl-login-button.js +++ b/injected/src/features/click-to-load/components/ctl-login-button.js @@ -1,7 +1,7 @@ -import { html } from '../../../dom-utils' -import cssVars from '../assets/shared.css' -import css from '../assets/ctl-login-button.css' -import { logoImg } from '../ctl-assets' +import { html } from '../../../dom-utils'; +import cssVars from '../assets/shared.css'; +import css from '../assets/ctl-login-button.css'; +import { logoImg } from '../ctl-assets'; /** * @typedef LearnMoreParams - "Learn More" link params @@ -20,7 +20,7 @@ export class DDGCtlLoginButton { * Placeholder container element for blocked login button * @type {HTMLDivElement} */ - #element + #element; /** * @param {object} params - Params for building a custom element with @@ -34,60 +34,60 @@ export class DDGCtlLoginButton { * @param {LearnMoreParams} params.learnMore - Localized strings for "Learn More" link. * @param {(originalElement: HTMLIFrameElement | HTMLElement, replacementElement: HTMLElement) => (e: any) => void} params.onClick */ - constructor (params) { - this.params = params + constructor(params) { + this.params = params; /** * Create the placeholder element to be inject in the page * @type {HTMLDivElement} */ - this.element = document.createElement('div') + this.element = document.createElement('div'); /** * Create the shadow root, closed to prevent any outside observers * @type {ShadowRoot} */ const shadow = this.element.attachShadow({ - mode: this.params.devMode ? 'open' : 'closed' - }) + mode: this.params.devMode ? 'open' : 'closed', + }); /** * Add our styles * @type {HTMLStyleElement} */ - const style = document.createElement('style') - style.innerText = cssVars + css + const style = document.createElement('style'); + style.innerText = cssVars + css; /** * Create the Facebook login button * @type {HTMLDivElement} */ - const loginButton = this._createLoginButton() + const loginButton = this._createLoginButton(); /** * Setup the click handlers */ - this._setupEventListeners(loginButton) + this._setupEventListeners(loginButton); /** * Append both to the shadow root */ - shadow.appendChild(loginButton) - shadow.appendChild(style) + shadow.appendChild(loginButton); + shadow.appendChild(style); } /** * @returns {HTMLDivElement} */ - get element () { - return this.#element + get element() { + return this.#element; } /** * @param {HTMLDivElement} el - New placeholder element */ - set element (el) { - this.#element = el + set element(el) { + this.#element = el; } /** @@ -96,14 +96,14 @@ export class DDGCtlLoginButton { * proceed. * @returns {HTMLDivElement} */ - _createLoginButton () { - const { label, hoverText, logoIcon, learnMore } = this.params + _createLoginButton() { + const { label, hoverText, logoIcon, learnMore } = this.params; - const { popoverStyle, arrowStyle } = this._calculatePopoverPosition() + const { popoverStyle, arrowStyle } = this._calculatePopoverPosition(); - const container = document.createElement('div') + const container = document.createElement('div'); // Add our own styles and inherit any local class styles on the button - container.classList.add('ddg-fb-login-container') + container.classList.add('ddg-fb-login-container'); container.innerHTML = html`
@@ -138,9 +138,9 @@ export class DDGCtlLoginButton {
- `.toString() + `.toString(); - return container + return container; } /** @@ -153,45 +153,43 @@ export class DDGCtlLoginButton { * arrowStyle: string, // CSS styles to be applied in the Popover arrow * }} */ - _calculatePopoverPosition () { - const { originalElement } = this.params - const rect = originalElement.getBoundingClientRect() - const textBubbleWidth = 360 // Should match the width rule in .ddg-popover - const textBubbleLeftShift = 100 // Should match the CSS left: rule in .ddg-popover - const arrowDefaultLocationPercent = 50 + _calculatePopoverPosition() { + const { originalElement } = this.params; + const rect = originalElement.getBoundingClientRect(); + const textBubbleWidth = 360; // Should match the width rule in .ddg-popover + const textBubbleLeftShift = 100; // Should match the CSS left: rule in .ddg-popover + const arrowDefaultLocationPercent = 50; - let popoverStyle - let arrowStyle + let popoverStyle; + let arrowStyle; if (rect.left < textBubbleLeftShift) { - const leftShift = -rect.left + 10 // 10px away from edge of the screen - popoverStyle = `left: ${leftShift}px;` - const change = (1 - rect.left / textBubbleLeftShift) * (100 - arrowDefaultLocationPercent) - arrowStyle = `left: ${Math.max(10, arrowDefaultLocationPercent - change)}%;` + const leftShift = -rect.left + 10; // 10px away from edge of the screen + popoverStyle = `left: ${leftShift}px;`; + const change = (1 - rect.left / textBubbleLeftShift) * (100 - arrowDefaultLocationPercent); + arrowStyle = `left: ${Math.max(10, arrowDefaultLocationPercent - change)}%;`; } else if (rect.left + textBubbleWidth - textBubbleLeftShift > window.innerWidth) { - const rightShift = rect.left + textBubbleWidth - textBubbleLeftShift - const diff = Math.min(rightShift - window.innerWidth, textBubbleLeftShift) - const rightMargin = 20 // Add some margin to the page, so scrollbar doesn't overlap. - popoverStyle = `left: -${textBubbleLeftShift + diff + rightMargin}px;` - const change = (diff / textBubbleLeftShift) * (100 - arrowDefaultLocationPercent) - arrowStyle = `left: ${Math.max(10, arrowDefaultLocationPercent + change)}%;` + const rightShift = rect.left + textBubbleWidth - textBubbleLeftShift; + const diff = Math.min(rightShift - window.innerWidth, textBubbleLeftShift); + const rightMargin = 20; // Add some margin to the page, so scrollbar doesn't overlap. + popoverStyle = `left: -${textBubbleLeftShift + diff + rightMargin}px;`; + const change = (diff / textBubbleLeftShift) * (100 - arrowDefaultLocationPercent); + arrowStyle = `left: ${Math.max(10, arrowDefaultLocationPercent + change)}%;`; } else { - popoverStyle = `left: -${textBubbleLeftShift}px;` - arrowStyle = `left: ${arrowDefaultLocationPercent}%;` + popoverStyle = `left: -${textBubbleLeftShift}px;`; + arrowStyle = `left: ${arrowDefaultLocationPercent}%;`; } - return { popoverStyle, arrowStyle } + return { popoverStyle, arrowStyle }; } /** * * @param {HTMLElement} loginButton */ - _setupEventListeners (loginButton) { - const { originalElement, onClick } = this.params + _setupEventListeners(loginButton) { + const { originalElement, onClick } = this.params; - loginButton - .querySelector('.ddg-ctl-fb-login-btn') - ?.addEventListener('click', onClick(originalElement, this.element)) + loginButton.querySelector('.ddg-ctl-fb-login-btn')?.addEventListener('click', onClick(originalElement, this.element)); } } diff --git a/injected/src/features/click-to-load/components/ctl-placeholder-blocked.js b/injected/src/features/click-to-load/components/ctl-placeholder-blocked.js index f88106943..67631e8e1 100644 --- a/injected/src/features/click-to-load/components/ctl-placeholder-blocked.js +++ b/injected/src/features/click-to-load/components/ctl-placeholder-blocked.js @@ -1,7 +1,7 @@ -import { html } from '../../../dom-utils' -import cssVars from '../assets/shared.css' -import css from '../assets/ctl-placeholder-block.css' -import { logoImg as daxImg } from '../ctl-assets' +import { html } from '../../../dom-utils'; +import cssVars from '../assets/shared.css'; +import css from '../assets/ctl-placeholder-block.css'; +import { logoImg as daxImg } from '../ctl-assets'; /** * Size keys for a placeholder @@ -34,26 +34,26 @@ import { logoImg as daxImg } from '../ctl-assets' * This is currently only used in our Mobile Apps, but can be expanded in the future. */ export class DDGCtlPlaceholderBlockedElement extends HTMLElement { - static CUSTOM_TAG_NAME = 'ddg-ctl-placeholder-blocked' + static CUSTOM_TAG_NAME = 'ddg-ctl-placeholder-blocked'; /** * Min height that the placeholder needs to have in order to * have enough room to display content. */ - static MIN_CONTENT_HEIGHT = 110 - static MAX_CONTENT_WIDTH_SMALL = 480 - static MAX_CONTENT_WIDTH_MEDIUM = 650 + static MIN_CONTENT_HEIGHT = 110; + static MAX_CONTENT_WIDTH_SMALL = 480; + static MAX_CONTENT_WIDTH_MEDIUM = 650; /** * Set observed attributes that will trigger attributeChangedCallback() */ - static get observedAttributes () { - return ['style'] + static get observedAttributes() { + return ['style']; } /** * Placeholder element for blocked content * @type {HTMLDivElement} */ - placeholderBlocked + placeholderBlocked; /** * Size variant of the latest calculated size of the placeholder. @@ -61,7 +61,7 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement { * and adapt the layout for each size. * @type {placeholderSize} */ - size = null + size = null; /** * @param {object} params - Params for building a custom element @@ -77,46 +77,46 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement { * @param {WithFeedbackParams=} params.withFeedback - Shows feedback link on tablet and desktop sizes, * @param {(originalElement: HTMLIFrameElement | HTMLElement, replacementElement: HTMLElement) => (e: any) => void} params.onButtonClick */ - constructor (params) { - super() - this.params = params + constructor(params) { + super(); + this.params = params; /** * Create the shadow root, closed to prevent any outside observers * @type {ShadowRoot} */ const shadow = this.attachShadow({ - mode: this.params.devMode ? 'open' : 'closed' - }) + mode: this.params.devMode ? 'open' : 'closed', + }); /** * Add our styles * @type {HTMLStyleElement} */ - const style = document.createElement('style') - style.innerText = cssVars + css + const style = document.createElement('style'); + style.innerText = cssVars + css; /** * Creates the placeholder for blocked content * @type {HTMLDivElement} */ - this.placeholderBlocked = this.createPlaceholder() + this.placeholderBlocked = this.createPlaceholder(); /** * Creates the Share Feedback element * @type {HTMLDivElement | null} */ - const feedbackLink = this.params.withFeedback ? this.createShareFeedbackLink() : null + const feedbackLink = this.params.withFeedback ? this.createShareFeedbackLink() : null; /** * Setup the click handlers */ - this.setupEventListeners(this.placeholderBlocked, feedbackLink) + this.setupEventListeners(this.placeholderBlocked, feedbackLink); /** * Append both to the shadow root */ // eslint-disable-next-line @typescript-eslint/no-unused-expressions - feedbackLink && this.placeholderBlocked.appendChild(feedbackLink) - shadow.appendChild(this.placeholderBlocked) - shadow.appendChild(style) + feedbackLink && this.placeholderBlocked.appendChild(feedbackLink); + shadow.appendChild(this.placeholderBlocked); + shadow.appendChild(style); } /** @@ -127,22 +127,20 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement { * @returns {HTMLDivElement} */ createPlaceholder = () => { - const { title, body, unblockBtnText, useSlimCard, withToggle, withFeedback } = this.params + const { title, body, unblockBtnText, useSlimCard, withToggle, withFeedback } = this.params; - const container = document.createElement('div') - container.classList.add('DuckDuckGoSocialContainer') + const container = document.createElement('div'); + container.classList.add('DuckDuckGoSocialContainer'); const cardClassNames = [ ['slim-card', !!useSlimCard], - ['with-feedback-link', !!withFeedback] + ['with-feedback-link', !!withFeedback], ] .map(([className, active]) => (active ? className : '')) - .join(' ') + .join(' '); // Only add a card footer if we have the toggle button to display - const cardFooterSection = withToggle - ? html` ` - : '' - const learnMoreLink = this.createLearnMoreLink() + const cardFooterSection = withToggle ? html` ` : ''; + const learnMoreLink = this.createLearnMoreLink(); container.innerHTML = html`
@@ -158,16 +156,16 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement {
${cardFooterSection} - `.toString() + `.toString(); - return container - } + return container; + }; /** * Creates a template string for Learn More link. */ createLearnMoreLink = () => { - const { learnMore } = this.params + const { learnMore } = this.params; return html`${learnMore.learnMore}` - } + >`; + }; /** * Creates a Feedback Link container row * @returns {HTMLDivElement} */ createShareFeedbackLink = () => { - const { withFeedback } = this.params + const { withFeedback } = this.params; - const container = document.createElement('div') - container.classList.add('ddg-ctl-feedback-row') + const container = document.createElement('div'); + container.classList.add('ddg-ctl-feedback-row'); container.innerHTML = html` - `.toString() + `.toString(); - return container - } + return container; + }; /** * Creates a template string for a toggle button with text. */ createToggleButton = () => { - const { withToggle } = this.params - if (!withToggle) return + const { withToggle } = this.params; + if (!withToggle) return; - const { isActive, dataKey, label, size: toggleSize = 'md' } = withToggle + const { isActive, dataKey, label, size: toggleSize = 'md' } = withToggle; const toggleButton = html`
@@ -217,9 +215,9 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement {
${label}
- ` - return toggleButton - } + `; + return toggleButton; + }; /** * @@ -227,21 +225,17 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement { * @param {HTMLElement?} feedbackLink */ setupEventListeners = (containerElement, feedbackLink) => { - const { withToggle, withFeedback, originalElement, onButtonClick } = this.params + const { withToggle, withFeedback, originalElement, onButtonClick } = this.params; - containerElement - .querySelector('button.ddg-ctl-unblock-btn') - ?.addEventListener('click', onButtonClick(originalElement, this)) + containerElement.querySelector('button.ddg-ctl-unblock-btn')?.addEventListener('click', onButtonClick(originalElement, this)); if (withToggle) { - containerElement - .querySelector('.ddg-toggle-button-container') - ?.addEventListener('click', withToggle.onClick) + containerElement.querySelector('.ddg-toggle-button-container')?.addEventListener('click', withToggle.onClick); } if (withFeedback && feedbackLink) { - feedbackLink.querySelector('.ddg-ctl-feedback-link')?.addEventListener('click', withFeedback.onClick) + feedbackLink.querySelector('.ddg-ctl-feedback-link')?.addEventListener('click', withFeedback.onClick); } - } + }; /** * Use JS to calculate the width and height of the root element placeholder. We could use a CSS Container Query, but full @@ -250,37 +244,37 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement { */ updatePlaceholderSize = () => { /** @type {placeholderSize} */ - let newSize = null + let newSize = null; - const { height, width } = this.getBoundingClientRect() + const { height, width } = this.getBoundingClientRect(); if (height && height < DDGCtlPlaceholderBlockedElement.MIN_CONTENT_HEIGHT) { - newSize = 'size-xs' + newSize = 'size-xs'; } else if (width) { if (width < DDGCtlPlaceholderBlockedElement.MAX_CONTENT_WIDTH_SMALL) { - newSize = 'size-sm' + newSize = 'size-sm'; } else if (width < DDGCtlPlaceholderBlockedElement.MAX_CONTENT_WIDTH_MEDIUM) { - newSize = 'size-md' + newSize = 'size-md'; } else { - newSize = 'size-lg' + newSize = 'size-lg'; } } if (newSize && newSize !== this.size) { if (this.size) { - this.placeholderBlocked.classList.remove(this.size) + this.placeholderBlocked.classList.remove(this.size); } - this.placeholderBlocked.classList.add(newSize) - this.size = newSize + this.placeholderBlocked.classList.add(newSize); + this.size = newSize; } - } + }; /** * Web Component lifecycle function. * When element is first added to the DOM, trigger this callback and * update the element CSS size class. */ - connectedCallback () { - this.updatePlaceholderSize() + connectedCallback() { + this.updatePlaceholderSize(); } /** @@ -292,10 +286,10 @@ export class DDGCtlPlaceholderBlockedElement extends HTMLElement { * @param {*} _ Attribute old value, ignored * @param {*} newValue Attribute new value */ - attributeChangedCallback (attr, _, newValue) { + attributeChangedCallback(attr, _, newValue) { if (attr === 'style') { - this.placeholderBlocked[attr].cssText = newValue - this.updatePlaceholderSize() + this.placeholderBlocked[attr].cssText = newValue; + this.updatePlaceholderSize(); } } } diff --git a/injected/src/features/click-to-load/components/index.js b/injected/src/features/click-to-load/components/index.js index 7e0e08952..6680ecc27 100644 --- a/injected/src/features/click-to-load/components/index.js +++ b/injected/src/features/click-to-load/components/index.js @@ -1,11 +1,11 @@ -import { DDGCtlPlaceholderBlockedElement } from './ctl-placeholder-blocked.js' +import { DDGCtlPlaceholderBlockedElement } from './ctl-placeholder-blocked.js'; /** * Register custom elements in this wrapper function to be called only when we need to * and also to allow remote-config later if needed. */ -export function registerCustomElements () { +export function registerCustomElements() { if (!customElements.get(DDGCtlPlaceholderBlockedElement.CUSTOM_TAG_NAME)) { - customElements.define(DDGCtlPlaceholderBlockedElement.CUSTOM_TAG_NAME, DDGCtlPlaceholderBlockedElement) + customElements.define(DDGCtlPlaceholderBlockedElement.CUSTOM_TAG_NAME, DDGCtlPlaceholderBlockedElement); } } diff --git a/injected/src/features/click-to-load/ctl-assets.js b/injected/src/features/click-to-load/ctl-assets.js index c9844d068..ccfa17d76 100644 --- a/injected/src/features/click-to-load/ctl-assets.js +++ b/injected/src/features/click-to-load/ctl-assets.js @@ -1,13 +1,22 @@ -export const logoImg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFQAAABUCAYAAAAcaxDBAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABNTSURBVHgBzV0LcFPXmf6PJFt+gkEY8wrYMSEbgst7m02ywZnOZiEJCQlJC+QB25lNs7OzlEJ2ptmZLGayfUy3EEhmW5rM7gCZBtjJgzxmSTvTRSST9IF5pCE0TUosmmBjHIKNZFmWLN2e78hHPvfqXuleSdfONyNLV7q6uve7//uc85vRlwAda25oTFK8lZGn0UPaLI2okUhrTH/KGnU7M+olTevlL0KaeM3e01LaKa/PE2p64dgpGmMwGgN0rGqtS1Ve2cB/fhk/gVbSqI5KAU4wvxlBTdNe9VJ5sOnAb0I0yhg1QiWJTGN3E0gcHQRTpO0dTXJdJ7RjzZJWflHrGaNVdiTRN2kalTfOIU9VLfnqp5ruM9TTxR+dlIqGKX7uI7IDLrl7PFS2zW1iXSMURGqkbaUc0uiprqWqxa1UOXcxVcxdxAmcRoUApMZDH9HAmeMU+8NxQbYV3Ca25ITCwaRY4immcYk0AUgcv3wtJ3CxeLgBEBw++jpF249akusWsSUltGPNoq0aY5vMVLviusU04b5HbJMoVLo/ItRaBUyBp7rGtjTHuNSGj75BkbdeN/2ckdbWdODENioRSkIopFLThl4hpi0wflZzy0pO5D9aEiDsIFfXQagtf4CAXCqronzWHHFc3CQ/f53rZuGYl198zorYEKOyW0shrUUT2rFu8bc1jdqMUplLIkFi9NhRCvOLA4mp/jCVAjAn+N2qJa1UvXSZkGYjQOylfTu4OQjqPxAhl7atef+JnVQEiiK0Y+2ipzSNq7gCXFT9o1vFRRkB6evnFxJ5642SkWgF4fD4OUxYba4dEW4GLr/0bJY2FGsCCiIUMaVWEX6FDB4cF1D/T1uzJANE4uTxPBaoWbbSlNgcZiDIYsl7mg6d6iWHcEyolb0MPLyFxq1Yq9sXqg31ihx9nb4MsCK298VnxQ3XQaNTjJXd49SuOiJUkEmJIyRy7TSgWg2bf5xlK/sO76defpJuq7ZTgMy61Y9Q7bI7de/Dlndvf8xoAhw7K9uECjX3R46okomTm/rEbt0dh1TixIzqDeI9lSPZD/ZDWDT0uT2PXmqYSSvI7HryUT2pkNTB5K121d82oZ+sWQzJbJXbZmRa3GWBces2UuXX7qOKigryeDy6z0A+wqbosaDIdEYLZtdgSiq3qVcfOH6rnWPaIlQE7MTacp1ImHvuL/Ztz63iE+qpZtN2qp8z13IX6Siix4OjYi7gQCdy+6+aADNSecKys3l/+3fyHc+bb4d0nMl+KLfNyIS9vPTfPyAtEbc8jvjevz5F45r/inIBpqF6aSvV/M1twiTYLX4UCpwzYlIRw17TMnIOS5aJ8E5eE5e8Gza2TO17+nTXb3IdLyehaSeUOsBfVsj3pv77z6hsWmNmH5AJycwFQeb3nqfBqvHU399P4XBYPMfjcWK8DOXz+bK+I4mFCo2GGRh479dZpFbMbhGkSvBzvWHTvFkHd53+zNKe5lR5bjc7SPHoE7h3rOPZjwTU/POftlE+4ORS5ZVEly+OvDm1UTw0bldRsmtoaCC/32/6/SvQgDw3rVSY9GibTv2zfps7qasPHl9o9X1LCYXd5HxnKkbIyQPrt2Q+h325uOOxnGqeOQfsE+vXvxnhN7krROzd/6PUlJkU9nOJrK4mrzf7lPxcaiCt0IxE57msgkkpAQdZNf9G8tYFMr8Ns5PoDKV3YDRl47zp7OnTnUGz75tK6HC82SG3jXbTwhM6Q0U1sZvvFERVz77e1PtbwSptLBVwndN/+PNMxocb+OnGu0acJM/7mVa20Cw+Nb2CFCW2qtsIhFUndPml5wq/mAmTiT2yjep2HKKZ/7CF6r+ylKqqqmyTCdRwlcQNRmXfDeDaEP5JgFjUJzLghSDUfM2+m3UVkE4uthvkNvJz1aZAOgpNJbWv3U/jnnyeZi5bQRMmTHBEohFprfmZa6RC9eFwJcCDmg2igI5RCeP3sq7IKJ2BhzdnXosY0Zjz2gHUm0vltAe/TYFAoCgiVUByQGqhQyf5gBxftddwyiqGh3j056RuGKUTjqhoVR8mc8bf/r2wk6VGmtTdIpIoNWRxRwISCk4UtBqlVEeoUTpRaZcAkYWoOtQ8MG+xaaxZKuCmj1u+ltwArlmtS6icABjRVbczhNqRTqfQFvGM57avU21t6aXnvTOd9PKb79O+l9rpnfYOGn/7WlekFFDNnBxykcDweMeqBZnRigyhmAqjHsSY2xbkiLh0Tpw4MbMZiQ5yAo7T1h2/oG89/iL9aHeQLvQ4jynfaQ8JEqsry6lhUi2dPXeJdr/4vmtSCgnVSalqS+HxK30b5GZGD73E1mvyTcNdKEg6m3hsOeWqjKqDuMf+43VOQA09vHoJNTcGqKbKL0h2ipuWNIqHEaloC115c78rRRUM3UhO8Cyyv+HfYZqG2TBiLEpIaDqQHynNVfHCwMhJhrMHtOzguqUi85GAet52y7W0/Ym7aP7caYJMQD6XAnBQmDjhBhAuqh7foA2tUu0FoVnqrngyjE4WdMeb5upy83uXt3DJdGdigwpjJb5UAJn9nAuJSsMIhVR7QejwBC4BqLsaLPcXIp0Az7vLy8szm1Pq3XEYRoh5US45J3UwT6q9BFf7VjynCfWMqDvGtVUUVDrjhWRx8BIF8FaQTk46OGxD7TEBwg1gQoaq9jrzwkjYSU/H/UsXqJMUVGcEz1aIumt1k/OSibDnP3cfoZ/se7cgTw/8ZN+vRdjUzb+/ekUL/fJouhjtFqFylouETu05h/BFnqQv1ah+ya+czKBL1XKQsIV7/F+89VFGygrx9t09V8RzJBrnEnpEhFOAf9a15BZUTjBjUEWSkq0ebj914+uq/SxmYkIqlbL87J3joczrmqp0Ovpue4icAtGCBGJRue1WwQRQJdRYQ2CkNfpI0+bLqqhRVYod4gWpZqof6R8pSr/85u/F880mcWU+IJ6Fs4NkNs8KZKIIT1UNuQWjTwGpsr6B9QE+D6M6GdAbp9Cod8MJWO9FzL+0JHT1innC/kmAlBsLIBRAbIuHCjte3sMVo2o2FyLuP+N8ZCbyAdmCsTgEIZTv8ZHhRp8mVlukRdQ4Pl0wBqLiCYNwZkWRe5d/RQT0cEwNnMx7V7RQKWE26068P0xi7fXc/l2l/8wuoQC4kVzpfwsqz1gdDYuoOqc9FY1QwcD4USxKiUTCchczySoVZGjjG8clqIGTN4M7qsnZJErEPiVHwPA2pSPDrHUAPquFBEXnw5zUoaEhKhpJfh69PEMZ5BoT78q/L394+H6z/oVLj42sNsWDi543yRFyDBI2ulek5KOEA5OnU8EY4Pb7Uz58Gy4s0rBLZtdBrsJ9VDK4R+jlnsIl9NIbRKE2chNQc0hmKckE3CP0Qkh4eTgmNafPi3ina2RCIsOnecHnT87tpl1wQrVQ1npKoqILDKzjA+HrBgYGnBHamb/2CmLiF7Pf940f/jyW3gfSl+DJ1BB/xP6cfi4FrKIIjNfrJBQr1Ea+VGRwzFUenn5w0OFxon/M+XHPYWchjhvAsh4JlTMuQb08rmchua16r5IMzXZ1UCwWc/adpHW4BiLHmkxAF6/rskkW8nC1PCc3jVMHiya185xwTI6cU611ETrp8N64AWN6rg+htD5O6IiEGrMjY23UMTrOiCfYUdsIWFfcx/PTKZ9MYwqjkKnpOefyFCc0FVJ3UEkttmoDxyR+NJ5/hl4GkNDASsuPpz/Mk5QVY0esWi82ajQv3Z3yeSkV1JRZjQNnTvBxmfRd8BdbqEUKygP8ft9sMQXHNq7azE+EO6eoeXGm5vr0A148zn3f4MW0V0+ZlFSRfiLILxufjgJkwA+v7zRDAlROsopHzBPyNR04Ffpk7eJemYKiBioHuuT4TFFpKFf7IT6+ZFV5MoWXhyXXvcBvxrPcsVnPpfINk4SCh2MUsOQN4ZIqoQNqKY+HTGjRIa5QS1FQvq8OGZdkfIYH+ACmgDvGtEeIWl7LaQIKQR/n4dIRcgzjWixdAV4jMSSaFhkPy4yPwmupO9beUtzFsDPHxLMjO6qinJufxq1pYhvbKOUp7AbDHIBI5O5fHEkH/06hrl+F/VT9Da/WH8KzCOw9/qE9WsybmUCKzgjyblRhVe/zRag97GhvD7ejPmd21AhO7BAfVTn/X9sxeCMKw3BM/vqRDEkFCEOWBBuLrMoss3ICaCtWOEuEs6YmpYL4Kwht2nOqt2PN4qCcPYKJ+hOGFyfgQDW33CneKxgfHKOhm253ZkdNgAmw8sYiF3crHzcDpFNNOdEtYgQsCF+EV5mrSzH2aua1Qe2rTZZqO0IxdlSBKOyOEdRpjMYmCYxSe+XrDKFQe9FkahjqFL5i+4MUbUfHGMapnWFl7VIaaXUHMoRC7bmnykip8S4Yp0M7grSjRUqom8PDuZBr4jGPvvZIdQd0Bo0XSvao2+o0RpPp0M4AO+o0rzfAqo+TEVE/o8MLy+hHd1fQQHlxXUDyTzxO6ro/6AhtOtAe5D8flNvG6dCB9ZsLr5MO5/XFSGmlDbMTvN5H2+73c0J99FmAie1CASKdSCdg4nKZjnHVlsLLFar6Mq93XM5TYMxUVFyqZfTMCj+9/NUynVT+9pq864MtYVyfpS5gSCOZ1Zsk69d2ne4MbWqZhuk5YtkwCqh+brvkglks1Ut378ozAmnEUEJMwk1yUurq9AOtF/o76YVP/ofe7v5/ev/ySUqk+LCJ10/Vvuzi9Nnuk/Re8iy9P8tLA34PNfSlhBTubS2n7rps+QC5X/04RZVxjZwg3R5pRHgw4bbvtT2Z7bR0ntxr/J7F0sQFjRrznpT5PSTjqmde0y3VO//dBxxPhtBu30DE49GpU6dSZWVl5v21h2+niC87cbi69hq6a+b91DJxIb392a/of//8PEWTepMBovq9Gnm81vHtA28nOKn2bbedpZiMkk1GdQdMzwI7ahrbJbdBYM9PR6QbxDZs+bFzezpsR41qf2HA/MZ8Ev6Ydn7wfXrglytp95mdWWQCkMBYbIA0zVoCv6ix75hwTcZ+AMb1Wbzuuc2MTPF9skDzgfY2fhsyDU5RNFGX6qFoEnhoMzmBtKNqwRnqXiwY81Aibj1LxQmhgYe2GMh81rgCJiS4sUDOPJBpyXvUYB+NBlSvj0YoaC9kG4hHOamQUDndcUr1NF7tym/ftBzTI7EkPJkjHBuwOeiKa6lR5uijAILliRlgFTIlc/YeyUmoUP2UpvNkxiYt6NXkiNTO9BCWGj5VeXOPjKLrg1bE53ZiUWPfKeOKZCCXqkvkrVQ0HzyxU2Oks6dGA40TwfJnOzaV/SGdhqpqP6V6ak4bCAlM8LTVah9I+1AiwR/mUjoxYn3sdGu5tiwys5q4cDKb97fn7Ytnq/TTvP/4JjXgN/tBqP/0H/w8/0hpV0iM10ej0cxbC+qXWpIhfo+rM8iMRvqFrcQjPhinAX6MSDhMc88O0sLzTLy+0ttHUS79g7FBcUyQXTFobi7kEvGaPB1xUE3KZTdV2I56Ny1peJWSnuX85RRspxeEHRXdY6Rkym4yObvZIB6dM5+0unqxOrmsrIy+iH1O73QeobLyMt2uIDHGJXmiN0Dfv/lp6rzyKSUScQqU1dOc2rnU0j+RVh3ppjs/9tEN5710z4c+uraH0cRwWmL7tDhFEjF6sJ1R3aBe7TGii4Y0+RthsVNscGjFrg8v2MpIHLZq4/EpeXWt2nBCaNVmLFzkamOh3XgH0R3rafz48aLoHEmE6Y5DN9G4upFKMSQQZK6evY6+Oe+fqaYs25zgpp3/7jpyAtx0ZHvGPn1wtt07HjMW0kNwQvnspgpHedmu0xd6N83jkso8raRIavhXL4lbo+baINhKWhk88l//HSWTSUEqsqKTF39H3dEu7q2TQpUDvkn0vZt20arZ3xCfm558XcBR1obsZ8rjT5v26et55t/0DWkgmSy5wgmZ4tqoAHRsWFBHMe8rmqHdpZO2ktoTe7jeVdGMGTPEZLKPL39IG498U5zQfXMepK9f+5CpVBoByep68ls597FqDisTluy1rCzIYkOj0+5Sxdk1S9qYoU2EVfdDQG3Dlly2WqSh6D2CBwDVt0OiEecfX5c1Rg7VxtBNtaFXiARI7Nm9LWusjJvtXc0Hj2+iAlF0y+Cz31i0iXnYVuPUcozBoF+JmdcXDu2zEEXG1YsYEk2wioHsbgYSy2fO4TdzZXpw0WTaoWVzWNEy2F5olAslamqd7awkrMxAKSGXDMp/KGCGdAOa58wbKQh7yVXcob00Q0kIlTAzARIgtparoFu9662Qs10xpJIXgezGmHZQUkKBYWlt4y/Xm30OSUWDA0ygcLPnEqbJXDls3d2BW5pDpCW/Uwqp1B2XXEI+YgHZigNeGJOwCiUY6hw7c0KQCGeTe1IGwzDPNgz3kAtwjVAJO8SqQFkQzgVk+yZZ/HOVz7sEacbpMJYQveq4RBLb6xaRIz81SgCxSfK0esmzXqN09wP3waWRpV6lgdSeQmLKgn6RxgAZcpnnbkFuCf9BFR8KD3K/f3Q0SdSfwpcAHevQVSLVmNLYAg+j+SBYLOrlNQ0TskP4k15swUIp0s5hFvZY/YcvI/4CeAZjCToTSnsAAAAASUVORK5CYII=' +export const logoImg = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFQAAABUCAYAAAAcaxDBAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABNTSURBVHgBzV0LcFPXmf6PJFt+gkEY8wrYMSEbgst7m02ywZnOZiEJCQlJC+QB25lNs7OzlEJ2ptmZLGayfUy3EEhmW5rM7gCZBtjJgzxmSTvTRSST9IF5pCE0TUosmmBjHIKNZFmWLN2e78hHPvfqXuleSdfONyNLV7q6uve7//uc85vRlwAda25oTFK8lZGn0UPaLI2okUhrTH/KGnU7M+olTevlL0KaeM3e01LaKa/PE2p64dgpGmMwGgN0rGqtS1Ve2cB/fhk/gVbSqI5KAU4wvxlBTdNe9VJ5sOnAb0I0yhg1QiWJTGN3E0gcHQRTpO0dTXJdJ7RjzZJWflHrGaNVdiTRN2kalTfOIU9VLfnqp5ruM9TTxR+dlIqGKX7uI7IDLrl7PFS2zW1iXSMURGqkbaUc0uiprqWqxa1UOXcxVcxdxAmcRoUApMZDH9HAmeMU+8NxQbYV3Ca25ITCwaRY4immcYk0AUgcv3wtJ3CxeLgBEBw++jpF249akusWsSUltGPNoq0aY5vMVLviusU04b5HbJMoVLo/ItRaBUyBp7rGtjTHuNSGj75BkbdeN/2ckdbWdODENioRSkIopFLThl4hpi0wflZzy0pO5D9aEiDsIFfXQagtf4CAXCqronzWHHFc3CQ/f53rZuGYl198zorYEKOyW0shrUUT2rFu8bc1jdqMUplLIkFi9NhRCvOLA4mp/jCVAjAn+N2qJa1UvXSZkGYjQOylfTu4OQjqPxAhl7atef+JnVQEiiK0Y+2ipzSNq7gCXFT9o1vFRRkB6evnFxJ5642SkWgF4fD4OUxYba4dEW4GLr/0bJY2FGsCCiIUMaVWEX6FDB4cF1D/T1uzJANE4uTxPBaoWbbSlNgcZiDIYsl7mg6d6iWHcEyolb0MPLyFxq1Yq9sXqg31ihx9nb4MsCK298VnxQ3XQaNTjJXd49SuOiJUkEmJIyRy7TSgWg2bf5xlK/sO76defpJuq7ZTgMy61Y9Q7bI7de/Dlndvf8xoAhw7K9uECjX3R46okomTm/rEbt0dh1TixIzqDeI9lSPZD/ZDWDT0uT2PXmqYSSvI7HryUT2pkNTB5K121d82oZ+sWQzJbJXbZmRa3GWBces2UuXX7qOKigryeDy6z0A+wqbosaDIdEYLZtdgSiq3qVcfOH6rnWPaIlQE7MTacp1ImHvuL/Ztz63iE+qpZtN2qp8z13IX6Siix4OjYi7gQCdy+6+aADNSecKys3l/+3fyHc+bb4d0nMl+KLfNyIS9vPTfPyAtEbc8jvjevz5F45r/inIBpqF6aSvV/M1twiTYLX4UCpwzYlIRw17TMnIOS5aJ8E5eE5e8Gza2TO17+nTXb3IdLyehaSeUOsBfVsj3pv77z6hsWmNmH5AJycwFQeb3nqfBqvHU399P4XBYPMfjcWK8DOXz+bK+I4mFCo2GGRh479dZpFbMbhGkSvBzvWHTvFkHd53+zNKe5lR5bjc7SPHoE7h3rOPZjwTU/POftlE+4ORS5ZVEly+OvDm1UTw0bldRsmtoaCC/32/6/SvQgDw3rVSY9GibTv2zfps7qasPHl9o9X1LCYXd5HxnKkbIyQPrt2Q+h325uOOxnGqeOQfsE+vXvxnhN7krROzd/6PUlJkU9nOJrK4mrzf7lPxcaiCt0IxE57msgkkpAQdZNf9G8tYFMr8Ns5PoDKV3YDRl47zp7OnTnUGz75tK6HC82SG3jXbTwhM6Q0U1sZvvFERVz77e1PtbwSptLBVwndN/+PNMxocb+OnGu0acJM/7mVa20Cw+Nb2CFCW2qtsIhFUndPml5wq/mAmTiT2yjep2HKKZ/7CF6r+ylKqqqmyTCdRwlcQNRmXfDeDaEP5JgFjUJzLghSDUfM2+m3UVkE4uthvkNvJz1aZAOgpNJbWv3U/jnnyeZi5bQRMmTHBEohFprfmZa6RC9eFwJcCDmg2igI5RCeP3sq7IKJ2BhzdnXosY0Zjz2gHUm0vltAe/TYFAoCgiVUByQGqhQyf5gBxftddwyiqGh3j056RuGKUTjqhoVR8mc8bf/r2wk6VGmtTdIpIoNWRxRwISCk4UtBqlVEeoUTpRaZcAkYWoOtQ8MG+xaaxZKuCmj1u+ltwArlmtS6icABjRVbczhNqRTqfQFvGM57avU21t6aXnvTOd9PKb79O+l9rpnfYOGn/7WlekFFDNnBxykcDweMeqBZnRigyhmAqjHsSY2xbkiLh0Tpw4MbMZiQ5yAo7T1h2/oG89/iL9aHeQLvQ4jynfaQ8JEqsry6lhUi2dPXeJdr/4vmtSCgnVSalqS+HxK30b5GZGD73E1mvyTcNdKEg6m3hsOeWqjKqDuMf+43VOQA09vHoJNTcGqKbKL0h2ipuWNIqHEaloC115c78rRRUM3UhO8Cyyv+HfYZqG2TBiLEpIaDqQHynNVfHCwMhJhrMHtOzguqUi85GAet52y7W0/Ym7aP7caYJMQD6XAnBQmDjhBhAuqh7foA2tUu0FoVnqrngyjE4WdMeb5upy83uXt3DJdGdigwpjJb5UAJn9nAuJSsMIhVR7QejwBC4BqLsaLPcXIp0Az7vLy8szm1Pq3XEYRoh5US45J3UwT6q9BFf7VjynCfWMqDvGtVUUVDrjhWRx8BIF8FaQTk46OGxD7TEBwg1gQoaq9jrzwkjYSU/H/UsXqJMUVGcEz1aIumt1k/OSibDnP3cfoZ/se7cgTw/8ZN+vRdjUzb+/ekUL/fJouhjtFqFylouETu05h/BFnqQv1ah+ya+czKBL1XKQsIV7/F+89VFGygrx9t09V8RzJBrnEnpEhFOAf9a15BZUTjBjUEWSkq0ebj914+uq/SxmYkIqlbL87J3joczrmqp0Ovpue4icAtGCBGJRue1WwQRQJdRYQ2CkNfpI0+bLqqhRVYod4gWpZqof6R8pSr/85u/F880mcWU+IJ6Fs4NkNs8KZKIIT1UNuQWjTwGpsr6B9QE+D6M6GdAbp9Cod8MJWO9FzL+0JHT1innC/kmAlBsLIBRAbIuHCjte3sMVo2o2FyLuP+N8ZCbyAdmCsTgEIZTv8ZHhRp8mVlukRdQ4Pl0wBqLiCYNwZkWRe5d/RQT0cEwNnMx7V7RQKWE26068P0xi7fXc/l2l/8wuoQC4kVzpfwsqz1gdDYuoOqc9FY1QwcD4USxKiUTCchczySoVZGjjG8clqIGTN4M7qsnZJErEPiVHwPA2pSPDrHUAPquFBEXnw5zUoaEhKhpJfh69PEMZ5BoT78q/L394+H6z/oVLj42sNsWDi543yRFyDBI2ulek5KOEA5OnU8EY4Pb7Uz58Gy4s0rBLZtdBrsJ9VDK4R+jlnsIl9NIbRKE2chNQc0hmKckE3CP0Qkh4eTgmNafPi3ina2RCIsOnecHnT87tpl1wQrVQ1npKoqILDKzjA+HrBgYGnBHamb/2CmLiF7Pf940f/jyW3gfSl+DJ1BB/xP6cfi4FrKIIjNfrJBQr1Ea+VGRwzFUenn5w0OFxon/M+XHPYWchjhvAsh4JlTMuQb08rmchua16r5IMzXZ1UCwWc/adpHW4BiLHmkxAF6/rskkW8nC1PCc3jVMHiya185xwTI6cU611ETrp8N64AWN6rg+htD5O6IiEGrMjY23UMTrOiCfYUdsIWFfcx/PTKZ9MYwqjkKnpOefyFCc0FVJ3UEkttmoDxyR+NJ5/hl4GkNDASsuPpz/Mk5QVY0esWi82ajQv3Z3yeSkV1JRZjQNnTvBxmfRd8BdbqEUKygP8ft9sMQXHNq7azE+EO6eoeXGm5vr0A148zn3f4MW0V0+ZlFSRfiLILxufjgJkwA+v7zRDAlROsopHzBPyNR04Ffpk7eJemYKiBioHuuT4TFFpKFf7IT6+ZFV5MoWXhyXXvcBvxrPcsVnPpfINk4SCh2MUsOQN4ZIqoQNqKY+HTGjRIa5QS1FQvq8OGZdkfIYH+ACmgDvGtEeIWl7LaQIKQR/n4dIRcgzjWixdAV4jMSSaFhkPy4yPwmupO9beUtzFsDPHxLMjO6qinJufxq1pYhvbKOUp7AbDHIBI5O5fHEkH/06hrl+F/VT9Da/WH8KzCOw9/qE9WsybmUCKzgjyblRhVe/zRag97GhvD7ejPmd21AhO7BAfVTn/X9sxeCMKw3BM/vqRDEkFCEOWBBuLrMoss3ICaCtWOEuEs6YmpYL4Kwht2nOqt2PN4qCcPYKJ+hOGFyfgQDW33CneKxgfHKOhm253ZkdNgAmw8sYiF3crHzcDpFNNOdEtYgQsCF+EV5mrSzH2aua1Qe2rTZZqO0IxdlSBKOyOEdRpjMYmCYxSe+XrDKFQe9FkahjqFL5i+4MUbUfHGMapnWFl7VIaaXUHMoRC7bmnykip8S4Yp0M7grSjRUqom8PDuZBr4jGPvvZIdQd0Bo0XSvao2+o0RpPp0M4AO+o0rzfAqo+TEVE/o8MLy+hHd1fQQHlxXUDyTzxO6ro/6AhtOtAe5D8flNvG6dCB9ZsLr5MO5/XFSGmlDbMTvN5H2+73c0J99FmAie1CASKdSCdg4nKZjnHVlsLLFar6Mq93XM5TYMxUVFyqZfTMCj+9/NUynVT+9pq864MtYVyfpS5gSCOZ1Zsk69d2ne4MbWqZhuk5YtkwCqh+brvkglks1Ut378ozAmnEUEJMwk1yUurq9AOtF/o76YVP/ofe7v5/ev/ySUqk+LCJ10/Vvuzi9Nnuk/Re8iy9P8tLA34PNfSlhBTubS2n7rps+QC5X/04RZVxjZwg3R5pRHgw4bbvtT2Z7bR0ntxr/J7F0sQFjRrznpT5PSTjqmde0y3VO//dBxxPhtBu30DE49GpU6dSZWVl5v21h2+niC87cbi69hq6a+b91DJxIb392a/of//8PEWTepMBovq9Gnm81vHtA28nOKn2bbedpZiMkk1GdQdMzwI7ahrbJbdBYM9PR6QbxDZs+bFzezpsR41qf2HA/MZ8Ev6Ydn7wfXrglytp95mdWWQCkMBYbIA0zVoCv6ix75hwTcZ+AMb1Wbzuuc2MTPF9skDzgfY2fhsyDU5RNFGX6qFoEnhoMzmBtKNqwRnqXiwY81Aibj1LxQmhgYe2GMh81rgCJiS4sUDOPJBpyXvUYB+NBlSvj0YoaC9kG4hHOamQUDndcUr1NF7tym/ftBzTI7EkPJkjHBuwOeiKa6lR5uijAILliRlgFTIlc/YeyUmoUP2UpvNkxiYt6NXkiNTO9BCWGj5VeXOPjKLrg1bE53ZiUWPfKeOKZCCXqkvkrVQ0HzyxU2Oks6dGA40TwfJnOzaV/SGdhqpqP6V6ak4bCAlM8LTVah9I+1AiwR/mUjoxYn3sdGu5tiwys5q4cDKb97fn7Ytnq/TTvP/4JjXgN/tBqP/0H/w8/0hpV0iM10ej0cxbC+qXWpIhfo+rM8iMRvqFrcQjPhinAX6MSDhMc88O0sLzTLy+0ttHUS79g7FBcUyQXTFobi7kEvGaPB1xUE3KZTdV2I56Ny1peJWSnuX85RRspxeEHRXdY6Rkym4yObvZIB6dM5+0unqxOrmsrIy+iH1O73QeobLyMt2uIDHGJXmiN0Dfv/lp6rzyKSUScQqU1dOc2rnU0j+RVh3ppjs/9tEN5710z4c+uraH0cRwWmL7tDhFEjF6sJ1R3aBe7TGii4Y0+RthsVNscGjFrg8v2MpIHLZq4/EpeXWt2nBCaNVmLFzkamOh3XgH0R3rafz48aLoHEmE6Y5DN9G4upFKMSQQZK6evY6+Oe+fqaYs25zgpp3/7jpyAtx0ZHvGPn1wtt07HjMW0kNwQvnspgpHedmu0xd6N83jkso8raRIavhXL4lbo+baINhKWhk88l//HSWTSUEqsqKTF39H3dEu7q2TQpUDvkn0vZt20arZ3xCfm558XcBR1obsZ8rjT5v26et55t/0DWkgmSy5wgmZ4tqoAHRsWFBHMe8rmqHdpZO2ktoTe7jeVdGMGTPEZLKPL39IG498U5zQfXMepK9f+5CpVBoByep68ls597FqDisTluy1rCzIYkOj0+5Sxdk1S9qYoU2EVfdDQG3Dlly2WqSh6D2CBwDVt0OiEecfX5c1Rg7VxtBNtaFXiARI7Nm9LWusjJvtXc0Hj2+iAlF0y+Cz31i0iXnYVuPUcozBoF+JmdcXDu2zEEXG1YsYEk2wioHsbgYSy2fO4TdzZXpw0WTaoWVzWNEy2F5olAslamqd7awkrMxAKSGXDMp/KGCGdAOa58wbKQh7yVXcob00Q0kIlTAzARIgtparoFu9662Qs10xpJIXgezGmHZQUkKBYWlt4y/Xm30OSUWDA0ygcLPnEqbJXDls3d2BW5pDpCW/Uwqp1B2XXEI+YgHZigNeGJOwCiUY6hw7c0KQCGeTe1IGwzDPNgz3kAtwjVAJO8SqQFkQzgVk+yZZ/HOVz7sEacbpMJYQveq4RBLb6xaRIz81SgCxSfK0esmzXqN09wP3waWRpV6lgdSeQmLKgn6RxgAZcpnnbkFuCf9BFR8KD3K/f3Q0SdSfwpcAHevQVSLVmNLYAg+j+SBYLOrlNQ0TskP4k15swUIp0s5hFvZY/YcvI/4CeAZjCToTSnsAAAAASUVORK5CYII='; export const loadingImages = { - darkMode: 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%40keyframes%20rotate%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20from%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%280deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20to%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%28359deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%20%20%20%20%3Cg%20style%3D%22transform-origin%3A%2050%25%2050%25%3B%20animation%3A%20rotate%201s%20infinite%20reverse%20linear%3B%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2218.0968%22%20y%3D%2216.0861%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%2018.0968%2016.0861%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.1%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.4%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2219.9976%22%20y%3D%228.37451%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%2019.9976%208.37451%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.2%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2216.1727%22%20y%3D%221.9917%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%2016.1727%201.9917%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.3%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.91309%22%20y%3D%226.88501%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%208.91309%206.88501%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.6%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%226.79602%22%20y%3D%2210.996%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%206.79602%2010.996%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.7%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%227%22%20y%3D%228.62549%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%207%208.62549%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.8%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20y%3D%2213%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.9%22%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fsvg%3E', - lightMode: 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%40keyframes%20rotate%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20from%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%280deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20to%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%28359deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%20%20%20%20%3Cg%20style%3D%22transform-origin%3A%2050%25%2050%25%3B%20animation%3A%20rotate%201s%20infinite%20reverse%20linear%3B%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2218.0968%22%20y%3D%2216.0861%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%2018.0968%2016.0861%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.1%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.4%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2219.9976%22%20y%3D%228.37451%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%2019.9976%208.37451%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.2%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2216.1727%22%20y%3D%221.9917%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%2016.1727%201.9917%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.3%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.91309%22%20y%3D%226.88501%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%208.91309%206.88501%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.6%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%226.79602%22%20y%3D%2210.996%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%206.79602%2010.996%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.7%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%227%22%20y%3D%228.62549%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%207%208.62549%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.8%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20y%3D%2213%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.9%22%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fsvg%3E' // 'data:application/octet-stream;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxzdHlsZT4KCQlAa2V5ZnJhbWVzIHJvdGF0ZSB7CgkJCWZyb20gewoJCQkJdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7CgkJCX0KCQkJdG8gewoJCQkJdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTsKCQkJfQoJCX0KCTwvc3R5bGU+Cgk8ZyBzdHlsZT0idHJhbnNmb3JtLW9yaWdpbjogNTAlIDUwJTsgYW5pbWF0aW9uOiByb3RhdGUgMXMgaW5maW5pdGUgcmV2ZXJzZSBsaW5lYXI7Ij4KCQk8cmVjdCB4PSIxOC4wOTY4IiB5PSIxNi4wODYxIiB3aWR0aD0iMyIgaGVpZ2h0PSI3IiByeD0iMS41IiB0cmFuc2Zvcm09InJvdGF0ZSgxMzYuMTYxIDE4LjA5NjggMTYuMDg2MSkiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC4xIi8+CQoJCTxyZWN0IHg9IjguNDk4NzgiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC40Ii8+CgkJPHJlY3QgeD0iMTkuOTk3NiIgeT0iOC4zNzQ1MSIgd2lkdGg9IjMiIGhlaWdodD0iNyIgcng9IjEuNSIgdHJhbnNmb3JtPSJyb3RhdGUoOTAgMTkuOTk3NiA4LjM3NDUxKSIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIwLjIiLz4KCQk8cmVjdCB4PSIxNi4xNzI3IiB5PSIxLjk5MTciIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDQ2LjE2MDcgMTYuMTcyNyAxLjk5MTcpIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjAuMyIvPgoJCTxyZWN0IHg9IjguOTEzMDkiIHk9IjYuODg1MDEiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDEzNi4xNjEgOC45MTMwOSA2Ljg4NTAxKSIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIwLjYiLz4KCQk8cmVjdCB4PSI2Ljc5NjAyIiB5PSIxMC45OTYiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDQ2LjE2MDcgNi43OTYwMiAxMC45OTYpIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjAuNyIvPgoJCTxyZWN0IHg9IjciIHk9IjguNjI1NDkiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDkwIDcgOC42MjU0OSkiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC44Ii8+CQkKCQk8cmVjdCB4PSI4LjQ5ODc4IiB5PSIxMyIgd2lkdGg9IjMiIGhlaWdodD0iNyIgcng9IjEuNSIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIwLjkiLz4KCTwvZz4KPC9zdmc+Cg==' -} -export const closeIcon = 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5.99998%204.58578L10.2426%200.34314C10.6331%20-0.0473839%2011.2663%20-0.0473839%2011.6568%200.34314C12.0474%200.733665%2012.0474%201.36683%2011.6568%201.75735L7.41419%205.99999L11.6568%2010.2426C12.0474%2010.6332%2012.0474%2011.2663%2011.6568%2011.6568C11.2663%2012.0474%2010.6331%2012.0474%2010.2426%2011.6568L5.99998%207.41421L1.75734%2011.6568C1.36681%2012.0474%200.733649%2012.0474%200.343125%2011.6568C-0.0473991%2011.2663%20-0.0473991%2010.6332%200.343125%2010.2426L4.58577%205.99999L0.343125%201.75735C-0.0473991%201.36683%20-0.0473991%200.733665%200.343125%200.34314C0.733649%20-0.0473839%201.36681%20-0.0473839%201.75734%200.34314L5.99998%204.58578Z%22%20fill%3D%22%23222222%22%2F%3E%0A%3C%2Fsvg%3E' + darkMode: + 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%40keyframes%20rotate%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20from%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%280deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20to%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%28359deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%20%20%20%20%3Cg%20style%3D%22transform-origin%3A%2050%25%2050%25%3B%20animation%3A%20rotate%201s%20infinite%20reverse%20linear%3B%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2218.0968%22%20y%3D%2216.0861%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%2018.0968%2016.0861%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.1%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.4%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2219.9976%22%20y%3D%228.37451%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%2019.9976%208.37451%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.2%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2216.1727%22%20y%3D%221.9917%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%2016.1727%201.9917%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.3%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.91309%22%20y%3D%226.88501%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%208.91309%206.88501%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.6%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%226.79602%22%20y%3D%2210.996%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%206.79602%2010.996%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.7%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%227%22%20y%3D%228.62549%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%207%208.62549%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.8%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20y%3D%2213%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.9%22%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fsvg%3E', + lightMode: + 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%20%20%20%20%3Cstyle%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%40keyframes%20rotate%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20from%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%280deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20to%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20transform%3A%20rotate%28359deg%29%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%3C%2Fstyle%3E%0A%20%20%20%20%20%20%20%20%3Cg%20style%3D%22transform-origin%3A%2050%25%2050%25%3B%20animation%3A%20rotate%201s%20infinite%20reverse%20linear%3B%22%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2218.0968%22%20y%3D%2216.0861%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%2018.0968%2016.0861%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.1%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.4%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2219.9976%22%20y%3D%228.37451%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%2019.9976%208.37451%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.2%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%2216.1727%22%20y%3D%221.9917%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%2016.1727%201.9917%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.3%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.91309%22%20y%3D%226.88501%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%28136.161%208.91309%206.88501%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.6%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%226.79602%22%20y%3D%2210.996%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2846.1607%206.79602%2010.996%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.7%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%227%22%20y%3D%228.62549%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20transform%3D%22rotate%2890%207%208.62549%29%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.8%22%2F%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Crect%20x%3D%228.49878%22%20y%3D%2213%22%20width%3D%223%22%20height%3D%227%22%20rx%3D%221.5%22%20fill%3D%22%23111111%22%20fill-opacity%3D%220.9%22%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2Fg%3E%0A%20%20%20%20%3C%2Fsvg%3E', // 'data:application/octet-stream;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KCTxzdHlsZT4KCQlAa2V5ZnJhbWVzIHJvdGF0ZSB7CgkJCWZyb20gewoJCQkJdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7CgkJCX0KCQkJdG8gewoJCQkJdHJhbnNmb3JtOiByb3RhdGUoMzU5ZGVnKTsKCQkJfQoJCX0KCTwvc3R5bGU+Cgk8ZyBzdHlsZT0idHJhbnNmb3JtLW9yaWdpbjogNTAlIDUwJTsgYW5pbWF0aW9uOiByb3RhdGUgMXMgaW5maW5pdGUgcmV2ZXJzZSBsaW5lYXI7Ij4KCQk8cmVjdCB4PSIxOC4wOTY4IiB5PSIxNi4wODYxIiB3aWR0aD0iMyIgaGVpZ2h0PSI3IiByeD0iMS41IiB0cmFuc2Zvcm09InJvdGF0ZSgxMzYuMTYxIDE4LjA5NjggMTYuMDg2MSkiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC4xIi8+CQoJCTxyZWN0IHg9IjguNDk4NzgiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC40Ii8+CgkJPHJlY3QgeD0iMTkuOTk3NiIgeT0iOC4zNzQ1MSIgd2lkdGg9IjMiIGhlaWdodD0iNyIgcng9IjEuNSIgdHJhbnNmb3JtPSJyb3RhdGUoOTAgMTkuOTk3NiA4LjM3NDUxKSIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIwLjIiLz4KCQk8cmVjdCB4PSIxNi4xNzI3IiB5PSIxLjk5MTciIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDQ2LjE2MDcgMTYuMTcyNyAxLjk5MTcpIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjAuMyIvPgoJCTxyZWN0IHg9IjguOTEzMDkiIHk9IjYuODg1MDEiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDEzNi4xNjEgOC45MTMwOSA2Ljg4NTAxKSIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIwLjYiLz4KCQk8cmVjdCB4PSI2Ljc5NjAyIiB5PSIxMC45OTYiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDQ2LjE2MDcgNi43OTYwMiAxMC45OTYpIiBmaWxsPSIjZmZmZmZmIiBmaWxsLW9wYWNpdHk9IjAuNyIvPgoJCTxyZWN0IHg9IjciIHk9IjguNjI1NDkiIHdpZHRoPSIzIiBoZWlnaHQ9IjciIHJ4PSIxLjUiIHRyYW5zZm9ybT0icm90YXRlKDkwIDcgOC42MjU0OSkiIGZpbGw9IiNmZmZmZmYiIGZpbGwtb3BhY2l0eT0iMC44Ii8+CQkKCQk8cmVjdCB4PSI4LjQ5ODc4IiB5PSIxMyIgd2lkdGg9IjMiIGhlaWdodD0iNyIgcng9IjEuNSIgZmlsbD0iI2ZmZmZmZiIgZmlsbC1vcGFjaXR5PSIwLjkiLz4KCTwvZz4KPC9zdmc+Cg==' +}; +export const closeIcon = + 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2212%22%20height%3D%2212%22%20viewBox%3D%220%200%2012%2012%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M5.99998%204.58578L10.2426%200.34314C10.6331%20-0.0473839%2011.2663%20-0.0473839%2011.6568%200.34314C12.0474%200.733665%2012.0474%201.36683%2011.6568%201.75735L7.41419%205.99999L11.6568%2010.2426C12.0474%2010.6332%2012.0474%2011.2663%2011.6568%2011.6568C11.2663%2012.0474%2010.6331%2012.0474%2010.2426%2011.6568L5.99998%207.41421L1.75734%2011.6568C1.36681%2012.0474%200.733649%2012.0474%200.343125%2011.6568C-0.0473991%2011.2663%20-0.0473991%2010.6332%200.343125%2010.2426L4.58577%205.99999L0.343125%201.75735C-0.0473991%201.36683%20-0.0473991%200.733665%200.343125%200.34314C0.733649%20-0.0473839%201.36681%20-0.0473839%201.75734%200.34314L5.99998%204.58578Z%22%20fill%3D%22%23222222%22%2F%3E%0A%3C%2Fsvg%3E'; -export const blockedFBLogo = 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2280%22%20height%3D%2280%22%20viewBox%3D%220%200%2080%2080%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Ccircle%20cx%3D%2240%22%20cy%3D%2240%22%20r%3D%2240%22%20fill%3D%22white%22%2F%3E%0A%3Cg%20clip-path%3D%22url%28%23clip0%29%22%3E%0A%3Cpath%20d%3D%22M73.8457%2039.974C73.8457%2021.284%2058.7158%206.15405%2040.0258%206.15405C21.3358%206.15405%206.15344%2021.284%206.15344%2039.974C6.15344%2056.884%2018.5611%2070.8622%2034.7381%2073.4275V49.764H26.0999V39.974H34.7381V32.5399C34.7381%2024.0587%2039.764%2019.347%2047.5122%2019.347C51.2293%2019.347%2055.0511%2020.0799%2055.0511%2020.0799V28.3517H50.8105C46.6222%2028.3517%2045.2611%2030.9693%2045.2611%2033.6393V39.974H54.6846L53.1664%2049.764H45.2611V73.4275C61.4381%2070.9146%2073.8457%2056.884%2073.8457%2039.974Z%22%20fill%3D%22%231877F2%22%2F%3E%0A%3C%2Fg%3E%0A%3Crect%20x%3D%223.01295%22%20y%3D%2211.7158%22%20width%3D%2212.3077%22%20height%3D%2292.3077%22%20rx%3D%226.15385%22%20transform%3D%22rotate%28-45%203.01295%2011.7158%29%22%20fill%3D%22%23666666%22%20stroke%3D%22white%22%20stroke-width%3D%226.15385%22%2F%3E%0A%3Cdefs%3E%0A%3CclipPath%20id%3D%22clip0%22%3E%0A%3Crect%20width%3D%2267.6923%22%20height%3D%2267.6923%22%20fill%3D%22white%22%20transform%3D%22translate%286.15344%206.15405%29%22%2F%3E%0A%3C%2FclipPath%3E%0A%3C%2Fdefs%3E%0A%3C%2Fsvg%3E' -export const facebookLogo = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMSAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguODUgMTkuOUM0LjEgMTkuMDUgMC41IDE0Ljk1IDAuNSAxMEMwLjUgNC41IDUgMCAxMC41IDBDMTYgMCAyMC41IDQuNSAyMC41IDEwQzIwLjUgMTQuOTUgMTYuOSAxOS4wNSAxMi4xNSAxOS45TDExLjYgMTkuNDVIOS40TDguODUgMTkuOVoiIGZpbGw9IiMxODc3RjIiLz4KPHBhdGggZD0iTTE0LjQgMTIuOEwxNC44NSAxMEgxMi4yVjguMDVDMTIuMiA3LjI1IDEyLjUgNi42NSAxMy43IDYuNjVIMTVWNC4xQzE0LjMgNCAxMy41IDMuOSAxMi44IDMuOUMxMC41IDMuOSA4LjkgNS4zIDguOSA3LjhWMTBINi40VjEyLjhIOC45VjE5Ljg1QzkuNDUgMTkuOTUgMTAgMjAgMTAuNTUgMjBDMTEuMSAyMCAxMS42NSAxOS45NSAxMi4yIDE5Ljg1VjEyLjhIMTQuNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=' +export const blockedFBLogo = + 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2280%22%20height%3D%2280%22%20viewBox%3D%220%200%2080%2080%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Ccircle%20cx%3D%2240%22%20cy%3D%2240%22%20r%3D%2240%22%20fill%3D%22white%22%2F%3E%0A%3Cg%20clip-path%3D%22url%28%23clip0%29%22%3E%0A%3Cpath%20d%3D%22M73.8457%2039.974C73.8457%2021.284%2058.7158%206.15405%2040.0258%206.15405C21.3358%206.15405%206.15344%2021.284%206.15344%2039.974C6.15344%2056.884%2018.5611%2070.8622%2034.7381%2073.4275V49.764H26.0999V39.974H34.7381V32.5399C34.7381%2024.0587%2039.764%2019.347%2047.5122%2019.347C51.2293%2019.347%2055.0511%2020.0799%2055.0511%2020.0799V28.3517H50.8105C46.6222%2028.3517%2045.2611%2030.9693%2045.2611%2033.6393V39.974H54.6846L53.1664%2049.764H45.2611V73.4275C61.4381%2070.9146%2073.8457%2056.884%2073.8457%2039.974Z%22%20fill%3D%22%231877F2%22%2F%3E%0A%3C%2Fg%3E%0A%3Crect%20x%3D%223.01295%22%20y%3D%2211.7158%22%20width%3D%2212.3077%22%20height%3D%2292.3077%22%20rx%3D%226.15385%22%20transform%3D%22rotate%28-45%203.01295%2011.7158%29%22%20fill%3D%22%23666666%22%20stroke%3D%22white%22%20stroke-width%3D%226.15385%22%2F%3E%0A%3Cdefs%3E%0A%3CclipPath%20id%3D%22clip0%22%3E%0A%3Crect%20width%3D%2267.6923%22%20height%3D%2267.6923%22%20fill%3D%22white%22%20transform%3D%22translate%286.15344%206.15405%29%22%2F%3E%0A%3C%2FclipPath%3E%0A%3C%2Fdefs%3E%0A%3C%2Fsvg%3E'; +export const facebookLogo = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMSAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguODUgMTkuOUM0LjEgMTkuMDUgMC41IDE0Ljk1IDAuNSAxMEMwLjUgNC41IDUgMCAxMC41IDBDMTYgMCAyMC41IDQuNSAyMC41IDEwQzIwLjUgMTQuOTUgMTYuOSAxOS4wNSAxMi4xNSAxOS45TDExLjYgMTkuNDVIOS40TDguODUgMTkuOVoiIGZpbGw9IiMxODc3RjIiLz4KPHBhdGggZD0iTTE0LjQgMTIuOEwxNC44NSAxMEgxMi4yVjguMDVDMTIuMiA3LjI1IDEyLjUgNi42NSAxMy43IDYuNjVIMTVWNC4xQzE0LjMgNCAxMy41IDMuOSAxMi44IDMuOUMxMC41IDMuOSA4LjkgNS4zIDguOSA3LjhWMTBINi40VjEyLjhIOC45VjE5Ljg1QzkuNDUgMTkuOTUgMTAgMjAgMTAuNTUgMjBDMTEuMSAyMCAxMS42NSAxOS45NSAxMi4yIDE5Ljg1VjEyLjhIMTQuNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='; -export const blockedYTVideo = 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2275%22%20height%3D%2275%22%20viewBox%3D%220%200%2075%2075%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Crect%20x%3D%226.75%22%20y%3D%2215.75%22%20width%3D%2256.25%22%20height%3D%2239%22%20rx%3D%2213.5%22%20fill%3D%22%23DE5833%22%2F%3E%0A%20%20%3Cmask%20id%3D%22path-2-outside-1_885_11045%22%20maskUnits%3D%22userSpaceOnUse%22%20x%3D%2223.75%22%20y%3D%2222.5%22%20width%3D%2224%22%20height%3D%2226%22%20fill%3D%22black%22%3E%0A%20%20%3Crect%20fill%3D%22white%22%20x%3D%2223.75%22%20y%3D%2222.5%22%20width%3D%2224%22%20height%3D%2226%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M41.9425%2037.5279C43.6677%2036.492%2043.6677%2033.9914%2041.9425%2032.9555L31.0394%2026.4088C29.262%2025.3416%2027%2026.6218%2027%2028.695L27%2041.7884C27%2043.8615%2029.262%2045.1418%2031.0394%2044.0746L41.9425%2037.5279Z%22%2F%3E%0A%20%20%3C%2Fmask%3E%0A%20%20%3Cpath%20d%3D%22M41.9425%2037.5279C43.6677%2036.492%2043.6677%2033.9914%2041.9425%2032.9555L31.0394%2026.4088C29.262%2025.3416%2027%2026.6218%2027%2028.695L27%2041.7884C27%2043.8615%2029.262%2045.1418%2031.0394%2044.0746L41.9425%2037.5279Z%22%20fill%3D%22white%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M30.0296%2044.6809L31.5739%2047.2529L30.0296%2044.6809ZM30.0296%2025.8024L31.5739%2023.2304L30.0296%2025.8024ZM42.8944%2036.9563L44.4387%2039.5283L42.8944%2036.9563ZM41.35%2036.099L28.4852%2028.3744L31.5739%2023.2304L44.4387%2030.955L41.35%2036.099ZM30%2027.5171L30%2042.9663L24%2042.9663L24%2027.5171L30%2027.5171ZM28.4852%2042.1089L41.35%2034.3843L44.4387%2039.5283L31.5739%2047.2529L28.4852%2042.1089ZM30%2042.9663C30%2042.1888%2029.1517%2041.7087%2028.4852%2042.1089L31.5739%2047.2529C28.2413%2049.2539%2024%2046.8535%2024%2042.9663L30%2042.9663ZM28.4852%2028.3744C29.1517%2028.7746%2030%2028.2945%2030%2027.5171L24%2027.5171C24%2023.6299%2028.2413%2021.2294%2031.5739%2023.2304L28.4852%2028.3744ZM44.4387%2030.955C47.6735%2032.8974%2047.6735%2037.586%2044.4387%2039.5283L41.35%2034.3843C40.7031%2034.7728%2040.7031%2035.7105%2041.35%2036.099L44.4387%2030.955Z%22%20fill%3D%22%23BC4726%22%20mask%3D%22url(%23path-2-outside-1_885_11045)%22%2F%3E%0A%20%20%3Ccircle%20cx%3D%2257.75%22%20cy%3D%2252.5%22%20r%3D%2213.5%22%20fill%3D%22%23E0E0E0%22%2F%3E%0A%20%20%3Crect%20x%3D%2248.75%22%20y%3D%2250.25%22%20width%3D%2218%22%20height%3D%224.5%22%20rx%3D%221.5%22%20fill%3D%22%23666666%22%2F%3E%0A%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M57.9853%2015.8781C58.2046%2016.1015%2058.5052%2016.2262%2058.8181%2016.2238C59.1311%2016.2262%2059.4316%2016.1015%2059.6509%2015.8781L62.9821%2012.5469C63.2974%2012.2532%2063.4272%2011.8107%2063.3206%2011.3931C63.2139%2010.9756%2062.8879%2010.6495%2062.4703%2010.5429C62.0528%2010.4363%2061.6103%2010.5661%2061.3165%2010.8813L57.9853%2014.2125C57.7627%2014.4325%2057.6374%2014.7324%2057.6374%2015.0453C57.6374%2015.3583%2057.7627%2015.6582%2057.9853%2015.8781ZM61.3598%2018.8363C61.388%2019.4872%2061.9385%2019.9919%2062.5893%2019.9637L62.6915%2019.9559L66.7769%2019.6023C67.4278%2019.5459%2067.9097%2018.9726%2067.8533%2018.3217C67.7968%2017.6708%2067.2235%2017.1889%2066.5726%2017.2453L62.4872%2017.6067C61.8363%2017.6349%2061.3316%2018.1854%2061.3598%2018.8363Z%22%20fill%3D%22%23AAAAAA%22%20fill-opacity%3D%220.6%22%2F%3E%0A%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M10.6535%2015.8781C10.4342%2016.1015%2010.1336%2016.2262%209.82067%2016.2238C9.5077%2016.2262%209.20717%2016.1015%208.98787%2015.8781L5.65667%2012.5469C5.34138%2012.2532%205.2116%2011.8107%205.31823%2011.3931C5.42487%2010.9756%205.75092%2010.6495%206.16847%2010.5429C6.58602%2010.4363%207.02848%2010.5661%207.32227%2010.8813L10.6535%2014.2125C10.8761%2014.4325%2011.0014%2014.7324%2011.0014%2015.0453C11.0014%2015.3583%2010.8761%2015.6582%2010.6535%2015.8781ZM7.2791%2018.8362C7.25089%2019.4871%206.7004%2019.9919%206.04954%2019.9637L5.9474%2019.9558L1.86197%2019.6023C1.44093%2019.5658%201.07135%2019.3074%200.892432%2018.9246C0.713515%2018.5417%200.752449%2018.0924%200.994567%2017.7461C1.23669%2017.3997%201.6452%2017.2088%202.06624%2017.2453L6.15167%2017.6067C6.80254%2017.6349%207.3073%2018.1854%207.2791%2018.8362Z%22%20fill%3D%22%23AAAAAA%22%20fill-opacity%3D%220.6%22%2F%3E%0A%3C%2Fsvg%3E%0A' -export const videoPlayDark = 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2222%22%20height%3D%2226%22%20viewBox%3D%220%200%2022%2026%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cpath%20d%3D%22M21%2011.2679C22.3333%2012.0377%2022.3333%2013.9622%2021%2014.732L3%2025.1244C1.66667%2025.8942%202.59376e-06%2024.9319%202.66105e-06%2023.3923L3.56958e-06%202.60769C3.63688e-06%201.06809%201.66667%200.105844%203%200.875644L21%2011.2679Z%22%20fill%3D%22%23222222%22%2F%3E%0A%3C%2Fsvg%3E%0A' -export const videoPlayLight = 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2222%22%20height%3D%2226%22%20viewBox%3D%220%200%2022%2026%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cpath%20d%3D%22M21%2011.2679C22.3333%2012.0377%2022.3333%2013.9622%2021%2014.732L3%2025.1244C1.66667%2025.8942%202.59376e-06%2024.9319%202.66105e-06%2023.3923L3.56958e-06%202.60769C3.63688e-06%201.06809%201.66667%200.105844%203%200.875644L21%2011.2679Z%22%20fill%3D%22%23FFFFFF%22%2F%3E%0A%3C%2Fsvg%3E' +export const blockedYTVideo = + 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2275%22%20height%3D%2275%22%20viewBox%3D%220%200%2075%2075%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Crect%20x%3D%226.75%22%20y%3D%2215.75%22%20width%3D%2256.25%22%20height%3D%2239%22%20rx%3D%2213.5%22%20fill%3D%22%23DE5833%22%2F%3E%0A%20%20%3Cmask%20id%3D%22path-2-outside-1_885_11045%22%20maskUnits%3D%22userSpaceOnUse%22%20x%3D%2223.75%22%20y%3D%2222.5%22%20width%3D%2224%22%20height%3D%2226%22%20fill%3D%22black%22%3E%0A%20%20%3Crect%20fill%3D%22white%22%20x%3D%2223.75%22%20y%3D%2222.5%22%20width%3D%2224%22%20height%3D%2226%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M41.9425%2037.5279C43.6677%2036.492%2043.6677%2033.9914%2041.9425%2032.9555L31.0394%2026.4088C29.262%2025.3416%2027%2026.6218%2027%2028.695L27%2041.7884C27%2043.8615%2029.262%2045.1418%2031.0394%2044.0746L41.9425%2037.5279Z%22%2F%3E%0A%20%20%3C%2Fmask%3E%0A%20%20%3Cpath%20d%3D%22M41.9425%2037.5279C43.6677%2036.492%2043.6677%2033.9914%2041.9425%2032.9555L31.0394%2026.4088C29.262%2025.3416%2027%2026.6218%2027%2028.695L27%2041.7884C27%2043.8615%2029.262%2045.1418%2031.0394%2044.0746L41.9425%2037.5279Z%22%20fill%3D%22white%22%2F%3E%0A%20%20%3Cpath%20d%3D%22M30.0296%2044.6809L31.5739%2047.2529L30.0296%2044.6809ZM30.0296%2025.8024L31.5739%2023.2304L30.0296%2025.8024ZM42.8944%2036.9563L44.4387%2039.5283L42.8944%2036.9563ZM41.35%2036.099L28.4852%2028.3744L31.5739%2023.2304L44.4387%2030.955L41.35%2036.099ZM30%2027.5171L30%2042.9663L24%2042.9663L24%2027.5171L30%2027.5171ZM28.4852%2042.1089L41.35%2034.3843L44.4387%2039.5283L31.5739%2047.2529L28.4852%2042.1089ZM30%2042.9663C30%2042.1888%2029.1517%2041.7087%2028.4852%2042.1089L31.5739%2047.2529C28.2413%2049.2539%2024%2046.8535%2024%2042.9663L30%2042.9663ZM28.4852%2028.3744C29.1517%2028.7746%2030%2028.2945%2030%2027.5171L24%2027.5171C24%2023.6299%2028.2413%2021.2294%2031.5739%2023.2304L28.4852%2028.3744ZM44.4387%2030.955C47.6735%2032.8974%2047.6735%2037.586%2044.4387%2039.5283L41.35%2034.3843C40.7031%2034.7728%2040.7031%2035.7105%2041.35%2036.099L44.4387%2030.955Z%22%20fill%3D%22%23BC4726%22%20mask%3D%22url(%23path-2-outside-1_885_11045)%22%2F%3E%0A%20%20%3Ccircle%20cx%3D%2257.75%22%20cy%3D%2252.5%22%20r%3D%2213.5%22%20fill%3D%22%23E0E0E0%22%2F%3E%0A%20%20%3Crect%20x%3D%2248.75%22%20y%3D%2250.25%22%20width%3D%2218%22%20height%3D%224.5%22%20rx%3D%221.5%22%20fill%3D%22%23666666%22%2F%3E%0A%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M57.9853%2015.8781C58.2046%2016.1015%2058.5052%2016.2262%2058.8181%2016.2238C59.1311%2016.2262%2059.4316%2016.1015%2059.6509%2015.8781L62.9821%2012.5469C63.2974%2012.2532%2063.4272%2011.8107%2063.3206%2011.3931C63.2139%2010.9756%2062.8879%2010.6495%2062.4703%2010.5429C62.0528%2010.4363%2061.6103%2010.5661%2061.3165%2010.8813L57.9853%2014.2125C57.7627%2014.4325%2057.6374%2014.7324%2057.6374%2015.0453C57.6374%2015.3583%2057.7627%2015.6582%2057.9853%2015.8781ZM61.3598%2018.8363C61.388%2019.4872%2061.9385%2019.9919%2062.5893%2019.9637L62.6915%2019.9559L66.7769%2019.6023C67.4278%2019.5459%2067.9097%2018.9726%2067.8533%2018.3217C67.7968%2017.6708%2067.2235%2017.1889%2066.5726%2017.2453L62.4872%2017.6067C61.8363%2017.6349%2061.3316%2018.1854%2061.3598%2018.8363Z%22%20fill%3D%22%23AAAAAA%22%20fill-opacity%3D%220.6%22%2F%3E%0A%20%20%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M10.6535%2015.8781C10.4342%2016.1015%2010.1336%2016.2262%209.82067%2016.2238C9.5077%2016.2262%209.20717%2016.1015%208.98787%2015.8781L5.65667%2012.5469C5.34138%2012.2532%205.2116%2011.8107%205.31823%2011.3931C5.42487%2010.9756%205.75092%2010.6495%206.16847%2010.5429C6.58602%2010.4363%207.02848%2010.5661%207.32227%2010.8813L10.6535%2014.2125C10.8761%2014.4325%2011.0014%2014.7324%2011.0014%2015.0453C11.0014%2015.3583%2010.8761%2015.6582%2010.6535%2015.8781ZM7.2791%2018.8362C7.25089%2019.4871%206.7004%2019.9919%206.04954%2019.9637L5.9474%2019.9558L1.86197%2019.6023C1.44093%2019.5658%201.07135%2019.3074%200.892432%2018.9246C0.713515%2018.5417%200.752449%2018.0924%200.994567%2017.7461C1.23669%2017.3997%201.6452%2017.2088%202.06624%2017.2453L6.15167%2017.6067C6.80254%2017.6349%207.3073%2018.1854%207.2791%2018.8362Z%22%20fill%3D%22%23AAAAAA%22%20fill-opacity%3D%220.6%22%2F%3E%0A%3C%2Fsvg%3E%0A'; +export const videoPlayDark = + 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2222%22%20height%3D%2226%22%20viewBox%3D%220%200%2022%2026%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cpath%20d%3D%22M21%2011.2679C22.3333%2012.0377%2022.3333%2013.9622%2021%2014.732L3%2025.1244C1.66667%2025.8942%202.59376e-06%2024.9319%202.66105e-06%2023.3923L3.56958e-06%202.60769C3.63688e-06%201.06809%201.66667%200.105844%203%200.875644L21%2011.2679Z%22%20fill%3D%22%23222222%22%2F%3E%0A%3C%2Fsvg%3E%0A'; +export const videoPlayLight = + 'data:image/svg+xml;utf8,%3Csvg%20width%3D%2222%22%20height%3D%2226%22%20viewBox%3D%220%200%2022%2026%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cpath%20d%3D%22M21%2011.2679C22.3333%2012.0377%2022.3333%2013.9622%2021%2014.732L3%2025.1244C1.66667%2025.8942%202.59376e-06%2024.9319%202.66105e-06%2023.3923L3.56958e-06%202.60769C3.63688e-06%201.06809%201.66667%200.105844%203%200.875644L21%2011.2679Z%22%20fill%3D%22%23FFFFFF%22%2F%3E%0A%3C%2Fsvg%3E'; diff --git a/injected/src/features/click-to-load/ctl-config.js b/injected/src/features/click-to-load/ctl-config.js index cc0c28be7..46d01400d 100644 --- a/injected/src/features/click-to-load/ctl-config.js +++ b/injected/src/features/click-to-load/ctl-config.js @@ -1,8 +1,6 @@ -import { - blockedFBLogo, blockedYTVideo, videoPlayDark, videoPlayLight -} from './ctl-assets.js' +import { blockedFBLogo, blockedYTVideo, videoPlayDark, videoPlayLight } from './ctl-assets.js'; -import localesJSON from '../../../../build/locales/ctl-locales.js' +import localesJSON from '../../../../build/locales/ctl-locales.js'; /********************************************************* * Style Definitions @@ -12,10 +10,11 @@ import localesJSON from '../../../../build/locales/ctl-locales.js' * (e.g. fonts.) * @param {import('../../content-feature.js').AssetConfig} [assets] */ -export function getStyles (assets) { - let fontStyle = '' - let regularFontFamily = "system, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'" - let boldFontFamily = regularFontFamily +export function getStyles(assets) { + let fontStyle = ''; + let regularFontFamily = + "system, -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"; + let boldFontFamily = regularFontFamily; if (assets?.regularFontUrl && assets?.boldFontUrl) { fontStyle = ` @font-face{ @@ -27,9 +26,9 @@ export function getStyles (assets) { font-weight: bold; src: url(${assets.boldFontUrl}); } - ` - regularFontFamily = 'DuckDuckGoPrivacyEssentials' - boldFontFamily = 'DuckDuckGoPrivacyEssentialsBold' + `; + regularFontFamily = 'DuckDuckGoPrivacyEssentials'; + boldFontFamily = 'DuckDuckGoPrivacyEssentialsBold'; } return { fontStyle, @@ -64,8 +63,8 @@ export function getStyles (assets) { `, inactive: ` background-color: #666666; - ` - } + `, + }, }, lightMode: { background: ` @@ -98,8 +97,8 @@ export function getStyles (assets) { `, inactive: ` background-color: #666666; - ` - } + `, + }, }, loginMode: { buttonBackground: ` @@ -107,7 +106,7 @@ export function getStyles (assets) { `, buttonFont: ` color: #FFFFFF; - ` + `, }, cancelMode: { buttonBackground: ` @@ -121,7 +120,7 @@ export function getStyles (assets) { `, buttonBackgroundPress: ` background: rgba(0, 0, 0, 0.18); - ` + `, }, button: ` border-radius: 8px; @@ -506,7 +505,7 @@ export function getStyles (assets) { `, inactive: ` left: 1px; - ` + `, }, placeholderWrapperDiv: ` position: relative; @@ -612,20 +611,20 @@ export function getStyles (assets) { `, youTubePreviewInfoText: ` color: #ABABAB; - ` - } + `, + }; } /** * @param {string} locale UI locale */ -export function getConfig (locale) { - const allLocales = JSON.parse(localesJSON) - const localeStrings = allLocales[locale] || allLocales.en +export function getConfig(locale) { + const allLocales = JSON.parse(localesJSON); + const localeStrings = allLocales[locale] || allLocales.en; - const fbStrings = localeStrings['facebook.json'] - const ytStrings = localeStrings['youtube.json'] - const sharedStrings = localeStrings['shared.json'] + const fbStrings = localeStrings['facebook.json']; + const ytStrings = localeStrings['youtube.json']; + const sharedStrings = localeStrings['shared.json']; const config = { 'Facebook, Inc.': { @@ -634,199 +633,187 @@ export function getConfig (locale) { messageTitle: fbStrings.informationalModalMessageTitle, messageBody: fbStrings.informationalModalMessageBody, confirmButtonText: fbStrings.informationalModalConfirmButtonText, - rejectButtonText: fbStrings.informationalModalRejectButtonText + rejectButtonText: fbStrings.informationalModalRejectButtonText, }, elementData: { 'FB Like Button': { - selectors: [ - '.fb-like' - ], + selectors: ['.fb-like'], replaceSettings: { - type: 'blank' - } + type: 'blank', + }, }, 'FB Button iFrames': { selectors: [ "iframe[src*='//www.facebook.com/plugins/like.php']", "iframe[src*='//www.facebook.com/v2.0/plugins/like.php']", "iframe[src*='//www.facebook.com/plugins/share_button.php']", - "iframe[src*='//www.facebook.com/v2.0/plugins/share_button.php']" + "iframe[src*='//www.facebook.com/v2.0/plugins/share_button.php']", ], replaceSettings: { - type: 'blank' - } + type: 'blank', + }, }, 'FB Save Button': { - selectors: [ - '.fb-save' - ], + selectors: ['.fb-save'], replaceSettings: { - type: 'blank' - } + type: 'blank', + }, }, 'FB Share Button': { - selectors: [ - '.fb-share-button' - ], + selectors: ['.fb-share-button'], replaceSettings: { - type: 'blank' - } + type: 'blank', + }, }, 'FB Page iFrames': { selectors: [ "iframe[src*='//www.facebook.com/plugins/page.php']", - "iframe[src*='//www.facebook.com/v2.0/plugins/page.php']" + "iframe[src*='//www.facebook.com/v2.0/plugins/page.php']", ], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockContent, infoTitle: fbStrings.infoTitleUnblockContent, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { - type: 'originalElement' - } + type: 'originalElement', + }, }, 'FB Page Div': { - selectors: [ - '.fb-page' - ], + selectors: ['.fb-page'], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockContent, infoTitle: fbStrings.infoTitleUnblockContent, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { type: 'iFrame', - targetURL: 'https://www.facebook.com/plugins/page.php?href=data-href&tabs=data-tabs&width=data-width&height=data-height', + targetURL: + 'https://www.facebook.com/plugins/page.php?href=data-href&tabs=data-tabs&width=data-width&height=data-height', urlDataAttributesToPreserve: { 'data-href': { default: '', - required: true + required: true, }, 'data-tabs': { - default: 'timeline' + default: 'timeline', }, 'data-height': { - default: '500' + default: '500', }, 'data-width': { - default: '500' - } + default: '500', + }, }, styleDataAttributes: { width: { name: 'data-width', - unit: 'px' + unit: 'px', }, height: { name: 'data-height', - unit: 'px' - } - } - } + unit: 'px', + }, + }, + }, }, 'FB Comment iFrames': { selectors: [ "iframe[src*='//www.facebook.com/plugins/comment_embed.php']", - "iframe[src*='//www.facebook.com/v2.0/plugins/comment_embed.php']" + "iframe[src*='//www.facebook.com/v2.0/plugins/comment_embed.php']", ], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockComment, infoTitle: fbStrings.infoTitleUnblockComment, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { - type: 'originalElement' - } + type: 'originalElement', + }, }, 'FB Comments': { - selectors: [ - '.fb-comments', - 'fb\\:comments' - ], + selectors: ['.fb-comments', 'fb\\:comments'], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockComments, infoTitle: fbStrings.infoTitleUnblockComments, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { type: 'allowFull', - targetURL: 'https://www.facebook.com/v9.0/plugins/comments.php?href=data-href&numposts=data-numposts&sdk=joey&version=v9.0&width=data-width', + targetURL: + 'https://www.facebook.com/v9.0/plugins/comments.php?href=data-href&numposts=data-numposts&sdk=joey&version=v9.0&width=data-width', urlDataAttributesToPreserve: { 'data-href': { default: '', - required: true + required: true, }, 'data-numposts': { - default: 10 + default: 10, }, 'data-width': { - default: '500' - } - } - } + default: '500', + }, + }, + }, }, 'FB Embedded Comment Div': { - selectors: [ - '.fb-comment-embed' - ], + selectors: ['.fb-comment-embed'], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockComment, infoTitle: fbStrings.infoTitleUnblockComment, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { type: 'iFrame', - targetURL: 'https://www.facebook.com/v9.0/plugins/comment_embed.php?href=data-href&sdk=joey&width=data-width&include_parent=data-include-parent', + targetURL: + 'https://www.facebook.com/v9.0/plugins/comment_embed.php?href=data-href&sdk=joey&width=data-width&include_parent=data-include-parent', urlDataAttributesToPreserve: { 'data-href': { default: '', - required: true + required: true, }, 'data-width': { - default: '500' + default: '500', }, 'data-include-parent': { - default: 'false' - } + default: 'false', + }, }, styleDataAttributes: { width: { name: 'data-width', - unit: 'px' - } - } - } + unit: 'px', + }, + }, + }, }, 'FB Post iFrames': { selectors: [ "iframe[src*='//www.facebook.com/plugins/post.php']", - "iframe[src*='//www.facebook.com/v2.0/plugins/post.php']" + "iframe[src*='//www.facebook.com/v2.0/plugins/post.php']", ], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockPost, infoTitle: fbStrings.infoTitleUnblockPost, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { - type: 'originalElement' - } + type: 'originalElement', + }, }, 'FB Posts Div': { - selectors: [ - '.fb-post' - ], + selectors: ['.fb-post'], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockPost, infoTitle: fbStrings.infoTitleUnblockPost, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { type: 'allowFull', @@ -834,49 +821,47 @@ export function getConfig (locale) { urlDataAttributesToPreserve: { 'data-href': { default: '', - required: true + required: true, }, 'data-width': { - default: '500' - } + default: '500', + }, }, styleDataAttributes: { width: { name: 'data-width', - unit: 'px' + unit: 'px', }, height: { name: 'data-height', unit: 'px', - fallbackAttribute: 'data-width' - } - } - } + fallbackAttribute: 'data-width', + }, + }, + }, }, 'FB Video iFrames': { selectors: [ "iframe[src*='//www.facebook.com/plugins/video.php']", - "iframe[src*='//www.facebook.com/v2.0/plugins/video.php']" + "iframe[src*='//www.facebook.com/v2.0/plugins/video.php']", ], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockVideo, infoTitle: fbStrings.infoTitleUnblockVideo, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { - type: 'originalElement' - } + type: 'originalElement', + }, }, 'FB Video': { - selectors: [ - '.fb-video' - ], + selectors: ['.fb-video'], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockVideo, infoTitle: fbStrings.infoTitleUnblockVideo, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { type: 'iFrame', @@ -884,49 +869,47 @@ export function getConfig (locale) { urlDataAttributesToPreserve: { 'data-href': { default: '', - required: true + required: true, }, 'data-width': { - default: '500' - } + default: '500', + }, }, styleDataAttributes: { width: { name: 'data-width', - unit: 'px' + unit: 'px', }, height: { name: 'data-height', unit: 'px', - fallbackAttribute: 'data-width' - } - } - } + fallbackAttribute: 'data-width', + }, + }, + }, }, 'FB Group iFrames': { selectors: [ "iframe[src*='//www.facebook.com/plugins/group.php']", - "iframe[src*='//www.facebook.com/v2.0/plugins/group.php']" + "iframe[src*='//www.facebook.com/v2.0/plugins/group.php']", ], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockContent, infoTitle: fbStrings.infoTitleUnblockContent, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { - type: 'originalElement' - } + type: 'originalElement', + }, }, 'FB Group': { - selectors: [ - '.fb-group' - ], + selectors: ['.fb-group'], replaceSettings: { type: 'dialog', buttonText: fbStrings.buttonTextUnblockContent, infoTitle: fbStrings.infoTitleUnblockContent, - infoText: fbStrings.infoTextUnblockContent + infoText: fbStrings.infoTextUnblockContent, }, clickAction: { type: 'iFrame', @@ -934,49 +917,48 @@ export function getConfig (locale) { urlDataAttributesToPreserve: { 'data-href': { default: '', - required: true + required: true, }, 'data-width': { - default: '500' - } + default: '500', + }, }, styleDataAttributes: { width: { name: 'data-width', - unit: 'px' - } - } - } + unit: 'px', + }, + }, + }, }, 'FB Login Button': { - selectors: [ - '.fb-login-button' - ], + selectors: ['.fb-login-button'], replaceSettings: { type: 'loginButton', icon: blockedFBLogo, buttonText: fbStrings.loginButtonText, buttonTextUnblockLogin: fbStrings.buttonTextUnblockLogin, - popupBodyText: fbStrings.loginBodyText + popupBodyText: fbStrings.loginBodyText, }, clickAction: { type: 'allowFull', - targetURL: 'https://www.facebook.com/v9.0/plugins/login_button.php?app_id=app_id_replace&auto_logout_link=false&button_type=continue_with&sdk=joey&size=large&use_continue_as=false&width=', + targetURL: + 'https://www.facebook.com/v9.0/plugins/login_button.php?app_id=app_id_replace&auto_logout_link=false&button_type=continue_with&sdk=joey&size=large&use_continue_as=false&width=', urlDataAttributesToPreserve: { 'data-href': { default: '', - required: true + required: true, }, 'data-width': { - default: '500' + default: '500', }, app_id_replace: { - default: 'null' - } - } - } - } - } + default: 'null', + }, + }, + }, + }, + }, }, Youtube: { informationalModal: { @@ -984,7 +966,7 @@ export function getConfig (locale) { messageTitle: ytStrings.informationalModalMessageTitle, messageBody: ytStrings.informationalModalMessageBody, confirmButtonText: ytStrings.informationalModalConfirmButtonText, - rejectButtonText: ytStrings.informationalModalRejectButtonText + rejectButtonText: ytStrings.informationalModalRejectButtonText, }, elementData: { 'YouTube embedded video': { @@ -996,7 +978,7 @@ export function getConfig (locale) { "iframe[data-src*='//youtube.com/embed']", "iframe[data-src*='//youtube-nocookie.com/embed']", "iframe[data-src*='//www.youtube.com/embed']", - "iframe[data-src*='//www.youtube-nocookie.com/embed']" + "iframe[data-src*='//www.youtube-nocookie.com/embed']", ], replaceSettings: { type: 'youtube-video', @@ -1010,13 +992,13 @@ export function getConfig (locale) { previewToggleEnabledDuckDuckGoText: ytStrings.infoPreviewToggleEnabledText, videoPlayIcon: { lightMode: videoPlayLight, - darkMode: videoPlayDark - } - } + darkMode: videoPlayDark, + }, + }, }, clickAction: { - type: 'youtube-video' - } + type: 'youtube-video', + }, }, 'YouTube embedded subscription button': { selectors: [ @@ -1027,15 +1009,15 @@ export function getConfig (locale) { "iframe[data-src*='//youtube.com/subscribe_embed']", "iframe[data-src*='//youtube-nocookie.com/subscribe_embed']", "iframe[data-src*='//www.youtube.com/subscribe_embed']", - "iframe[data-src*='//www.youtube-nocookie.com/subscribe_embed']" + "iframe[data-src*='//www.youtube-nocookie.com/subscribe_embed']", ], replaceSettings: { - type: 'blank' - } - } - } - } - } + type: 'blank', + }, + }, + }, + }, + }; - return { config, sharedStrings } + return { config, sharedStrings }; } diff --git a/injected/src/features/cookie.js b/injected/src/features/cookie.js index 0d6aa9432..73647ca2e 100644 --- a/injected/src/features/cookie.js +++ b/injected/src/features/cookie.js @@ -1,7 +1,15 @@ -import { postDebugMessage, getStackTraceOrigins, getStack, isBeingFramed, isThirdPartyFrame, getTabHostname, matchHostname } from '../utils.js' -import { Cookie } from '../cookie.js' -import ContentFeature from '../content-feature.js' -import { isTrackerOrigin } from '../trackers.js' +import { + postDebugMessage, + getStackTraceOrigins, + getStack, + isBeingFramed, + isThirdPartyFrame, + getTabHostname, + matchHostname, +} from '../utils.js'; +import { Cookie } from '../cookie.js'; +import ContentFeature from '../content-feature.js'; +import { isTrackerOrigin } from '../trackers.js'; /** * @typedef ExtensionCookiePolicy @@ -11,9 +19,9 @@ import { isTrackerOrigin } from '../trackers.js' * @property {boolean} isThirdPartyFrame */ -function initialShouldBlockTrackerCookie () { - const injectName = import.meta.injectName - return injectName === 'chrome' || injectName === 'firefox' || injectName === 'chrome-mv3' || injectName === 'windows' +function initialShouldBlockTrackerCookie() { + const injectName = import.meta.injectName; + return injectName === 'chrome' || injectName === 'firefox' || injectName === 'chrome-mv3' || injectName === 'windows'; } // Initial cookie policy pre init @@ -27,236 +35,237 @@ let cookiePolicy = { isThirdPartyFrame: isThirdPartyFrame(), policy: { threshold: 604800, // 7 days - maxAge: 604800 // 7 days + maxAge: 604800, // 7 days }, trackerPolicy: { threshold: 86400, // 1 day - maxAge: 86400 // 1 day + maxAge: 86400, // 1 day }, - allowlist: /** @type {{ host: string }[]} */([]) -} -let trackerLookup = {} + allowlist: /** @type {{ host: string }[]} */ ([]), +}; +let trackerLookup = {}; -let loadedPolicyResolve +let loadedPolicyResolve; /** * @param {'ignore' | 'block' | 'restrict'} action * @param {string} reason * @param {any} ctx */ -function debugHelper (action, reason, ctx) { +function debugHelper(action, reason, ctx) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions - cookiePolicy.debug && postDebugMessage('jscookie', { - action, - reason, - stack: ctx.stack, - documentUrl: globalThis.document.location.href, - value: ctx.value - }) + cookiePolicy.debug && + postDebugMessage('jscookie', { + action, + reason, + stack: ctx.stack, + documentUrl: globalThis.document.location.href, + value: ctx.value, + }); } /** * @returns {boolean} */ -function shouldBlockTrackingCookie () { - return cookiePolicy.shouldBlock && cookiePolicy.shouldBlockTrackerCookie && isTrackingCookie() +function shouldBlockTrackingCookie() { + return cookiePolicy.shouldBlock && cookiePolicy.shouldBlockTrackerCookie && isTrackingCookie(); } -function shouldBlockNonTrackingCookie () { - return cookiePolicy.shouldBlock && cookiePolicy.shouldBlockNonTrackerCookie && isNonTrackingCookie() +function shouldBlockNonTrackingCookie() { + return cookiePolicy.shouldBlock && cookiePolicy.shouldBlockNonTrackerCookie && isNonTrackingCookie(); } /** * @param {Set} scriptOrigins * @returns {boolean} */ -function isFirstPartyTrackerScript (scriptOrigins) { - let matched = false +function isFirstPartyTrackerScript(scriptOrigins) { + let matched = false; for (const scriptOrigin of scriptOrigins) { if (cookiePolicy.allowlist.find((allowlistOrigin) => matchHostname(allowlistOrigin.host, scriptOrigin))) { - return false + return false; } if (isTrackerOrigin(trackerLookup, scriptOrigin)) { - matched = true + matched = true; } } - return matched + return matched; } /** * @returns {boolean} */ -function isTrackingCookie () { - return cookiePolicy.isFrame && cookiePolicy.isTracker && cookiePolicy.isThirdPartyFrame +function isTrackingCookie() { + return cookiePolicy.isFrame && cookiePolicy.isTracker && cookiePolicy.isThirdPartyFrame; } -function isNonTrackingCookie () { - return cookiePolicy.isFrame && !cookiePolicy.isTracker && cookiePolicy.isThirdPartyFrame +function isNonTrackingCookie() { + return cookiePolicy.isFrame && !cookiePolicy.isTracker && cookiePolicy.isThirdPartyFrame; } export default class CookieFeature extends ContentFeature { - load () { + load() { if (this.documentOriginIsTracker) { - cookiePolicy.isTracker = true + cookiePolicy.isTracker = true; } if (this.trackerLookup) { - trackerLookup = this.trackerLookup + trackerLookup = this.trackerLookup; } if (this.bundledConfig?.features?.cookie) { // use the bundled config to get a best-effort at the policy, before the background sends the real one - const { exceptions, settings } = this.bundledConfig.features.cookie - const tabHostname = getTabHostname() - let tabExempted = true + const { exceptions, settings } = this.bundledConfig.features.cookie; + const tabHostname = getTabHostname(); + let tabExempted = true; if (tabHostname != null) { tabExempted = exceptions.some((exception) => { - return matchHostname(tabHostname, exception.domain) - }) + return matchHostname(tabHostname, exception.domain); + }); } const frameExempted = settings.excludedCookieDomains.some((exception) => { - return matchHostname(globalThis.location.hostname, exception.domain) - }) - cookiePolicy.shouldBlock = !frameExempted && !tabExempted - cookiePolicy.policy = settings.firstPartyCookiePolicy - cookiePolicy.trackerPolicy = settings.firstPartyTrackerCookiePolicy + return matchHostname(globalThis.location.hostname, exception.domain); + }); + cookiePolicy.shouldBlock = !frameExempted && !tabExempted; + cookiePolicy.policy = settings.firstPartyCookiePolicy; + cookiePolicy.trackerPolicy = settings.firstPartyTrackerCookiePolicy; // Allows for ad click conversion detection as described by https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/. // This only applies when the resources that would set these cookies are unblocked. - cookiePolicy.allowlist = this.getFeatureSetting('allowlist', 'adClickAttribution') || [] + cookiePolicy.allowlist = this.getFeatureSetting('allowlist', 'adClickAttribution') || []; } // The cookie policy is injected into every frame immediately so that no cookie will // be missed. - const document = globalThis.document + const document = globalThis.document; // @ts-expect-error - Object is possibly 'undefined'. - const cookieSetter = Object.getOwnPropertyDescriptor(globalThis.Document.prototype, 'cookie').set + const cookieSetter = Object.getOwnPropertyDescriptor(globalThis.Document.prototype, 'cookie').set; // @ts-expect-error - Object is possibly 'undefined'. - const cookieGetter = Object.getOwnPropertyDescriptor(globalThis.Document.prototype, 'cookie').get + const cookieGetter = Object.getOwnPropertyDescriptor(globalThis.Document.prototype, 'cookie').get; const loadPolicy = new Promise((resolve) => { - loadedPolicyResolve = resolve - }) + loadedPolicyResolve = resolve; + }); // Create the then callback now - this ensures that Promise.prototype.then changes won't break // this call. - const loadPolicyThen = loadPolicy.then.bind(loadPolicy) + const loadPolicyThen = loadPolicy.then.bind(loadPolicy); - function getCookiePolicy () { - let getCookieContext = null + function getCookiePolicy() { + let getCookieContext = null; if (cookiePolicy.debug) { - const stack = getStack() + const stack = getStack(); getCookieContext = { stack, - value: 'getter' - } + value: 'getter', + }; } if (shouldBlockTrackingCookie() || shouldBlockNonTrackingCookie()) { - debugHelper('block', '3p frame', getCookieContext) - return '' + debugHelper('block', '3p frame', getCookieContext); + return ''; } else if (isTrackingCookie() || isNonTrackingCookie()) { - debugHelper('ignore', '3p frame', getCookieContext) + debugHelper('ignore', '3p frame', getCookieContext); } // @ts-expect-error - error TS18048: 'cookieGetter' is possibly 'undefined'. - return cookieGetter.call(this) + return cookieGetter.call(this); } /** * @param {any} argValue */ - function setCookiePolicy (argValue) { - let setCookieContext = null + function setCookiePolicy(argValue) { + let setCookieContext = null; if (!argValue?.toString || typeof argValue.toString() !== 'string') { // not a string, or string-like - return + return; } - const value = argValue.toString() + const value = argValue.toString(); if (cookiePolicy.debug) { - const stack = getStack() + const stack = getStack(); setCookieContext = { stack, - value - } + value, + }; } if (shouldBlockTrackingCookie() || shouldBlockNonTrackingCookie()) { - debugHelper('block', '3p frame', setCookieContext) - return + debugHelper('block', '3p frame', setCookieContext); + return; } else if (isTrackingCookie() || isNonTrackingCookie()) { - debugHelper('ignore', '3p frame', setCookieContext) + debugHelper('ignore', '3p frame', setCookieContext); } // call the native document.cookie implementation. This will set the cookie immediately // if the value is valid. We will override this set later if the policy dictates that // the expiry should be changed. // @ts-expect-error - error TS18048: 'cookieSetter' is possibly 'undefined'. - cookieSetter.call(this, argValue) + cookieSetter.call(this, argValue); try { // wait for config before doing same-site tests loadPolicyThen(() => { - const { shouldBlock, policy, trackerPolicy } = cookiePolicy - const stack = getStack() - const scriptOrigins = getStackTraceOrigins(stack) - const chosenPolicy = isFirstPartyTrackerScript(scriptOrigins) ? trackerPolicy : policy + const { shouldBlock, policy, trackerPolicy } = cookiePolicy; + const stack = getStack(); + const scriptOrigins = getStackTraceOrigins(stack); + const chosenPolicy = isFirstPartyTrackerScript(scriptOrigins) ? trackerPolicy : policy; if (!shouldBlock) { - debugHelper('ignore', 'disabled', setCookieContext) - return + debugHelper('ignore', 'disabled', setCookieContext); + return; } // extract cookie expiry from cookie string - const cookie = new Cookie(value) + const cookie = new Cookie(value); // apply cookie policy if (cookie.getExpiry() > chosenPolicy.threshold) { // check if the cookie still exists - if (document.cookie.split(';').findIndex(kv => kv.trim().startsWith(cookie.parts[0].trim())) !== -1) { - cookie.maxAge = chosenPolicy.maxAge + if (document.cookie.split(';').findIndex((kv) => kv.trim().startsWith(cookie.parts[0].trim())) !== -1) { + cookie.maxAge = chosenPolicy.maxAge; - debugHelper('restrict', 'expiry', setCookieContext) + debugHelper('restrict', 'expiry', setCookieContext); // @ts-expect-error - error TS18048: 'cookieSetter' is possibly 'undefined'. - cookieSetter.apply(document, [cookie.toString()]) + cookieSetter.apply(document, [cookie.toString()]); } else { - debugHelper('ignore', 'dissappeared', setCookieContext) + debugHelper('ignore', 'dissappeared', setCookieContext); } } else { - debugHelper('ignore', 'expiry', setCookieContext) + debugHelper('ignore', 'expiry', setCookieContext); } - }) + }); } catch (e) { - debugHelper('ignore', 'error', setCookieContext) + debugHelper('ignore', 'error', setCookieContext); // suppress error in cookie override to avoid breakage - console.warn('Error in cookie override', e) + console.warn('Error in cookie override', e); } } this.wrapProperty(globalThis.Document.prototype, 'cookie', { set: setCookiePolicy, - get: getCookiePolicy - }) + get: getCookiePolicy, + }); } - init (args) { + init(args) { const restOfPolicy = { debug: this.isDebug, shouldBlockTrackerCookie: this.getFeatureSettingEnabled('trackerCookie'), shouldBlockNonTrackerCookie: this.getFeatureSettingEnabled('nonTrackerCookie'), allowlist: this.getFeatureSetting('allowlist', 'adClickAttribution') || [], policy: this.getFeatureSetting('firstPartyCookiePolicy'), - trackerPolicy: this.getFeatureSetting('firstPartyTrackerCookiePolicy') - } + trackerPolicy: this.getFeatureSetting('firstPartyTrackerCookiePolicy'), + }; // The extension provides some additional info about the cookie policy, let's use that over our guesses if (args.cookie) { - const extensionCookiePolicy = /** @type {ExtensionCookiePolicy} */(args.cookie) + const extensionCookiePolicy = /** @type {ExtensionCookiePolicy} */ (args.cookie); cookiePolicy = { ...extensionCookiePolicy, - ...restOfPolicy - } + ...restOfPolicy, + }; } else { // copy non-null entries from restOfPolicy to cookiePolicy - Object.keys(restOfPolicy).forEach(key => { + Object.keys(restOfPolicy).forEach((key) => { if (restOfPolicy[key]) { - cookiePolicy[key] = restOfPolicy[key] + cookiePolicy[key] = restOfPolicy[key]; } - }) + }); } - loadedPolicyResolve() + loadedPolicyResolve(); } } diff --git a/injected/src/features/duck-player.js b/injected/src/features/duck-player.js index 13c7019a4..f7496bc26 100644 --- a/injected/src/features/duck-player.js +++ b/injected/src/features/duck-player.js @@ -31,11 +31,11 @@ * * @module Duck Player Overlays */ -import ContentFeature from '../content-feature.js' +import ContentFeature from '../content-feature.js'; -import { DuckPlayerOverlayMessages, OpenInDuckPlayerMsg, Pixel } from './duckplayer/overlay-messages.js' -import { isBeingFramed } from '../utils.js' -import { Environment, initOverlays } from './duckplayer/overlays.js' +import { DuckPlayerOverlayMessages, OpenInDuckPlayerMsg, Pixel } from './duckplayer/overlay-messages.js'; +import { isBeingFramed } from '../utils.js'; +import { Environment, initOverlays } from './duckplayer/overlays.js'; /** * @typedef UserValues - A way to communicate user settings @@ -59,30 +59,29 @@ import { Environment, initOverlays } from './duckplayer/overlays.js' * @internal */ export default class DuckPlayerFeature extends ContentFeature { - - init (args) { + init(args) { /** * This feature never operates in a frame */ - if (isBeingFramed()) return + if (isBeingFramed()) return; /** * Just the 'overlays' part of the settings object. * @type {import("../types/duckplayer-settings.js").DuckPlayerSettings['overlays']} */ - const overlaySettings = this.getFeatureSetting('overlays') - const overlaysEnabled = overlaySettings?.youtube?.state === 'enabled' + const overlaySettings = this.getFeatureSetting('overlays'); + const overlaysEnabled = overlaySettings?.youtube?.state === 'enabled'; /** * Serp proxy */ - const serpProxyEnabled = overlaySettings?.serpProxy?.state === 'enabled' + const serpProxyEnabled = overlaySettings?.serpProxy?.state === 'enabled'; /** * Bail if no features are enabled */ if (!overlaysEnabled && !serpProxyEnabled) { - return + return; } /** @@ -90,27 +89,27 @@ export default class DuckPlayerFeature extends ContentFeature { * accidentally enabled this */ if (!this.messaging) { - throw new Error('cannot operate duck player without a messaging backend') + throw new Error('cannot operate duck player without a messaging backend'); } - const locale = args?.locale || args?.language || 'en' + const locale = args?.locale || args?.language || 'en'; const env = new Environment({ debug: args.debug, injectName: import.meta.injectName, platform: this.platform, - locale - }) - const comms = new DuckPlayerOverlayMessages(this.messaging, env) + locale, + }); + const comms = new DuckPlayerOverlayMessages(this.messaging, env); if (overlaysEnabled) { - initOverlays(overlaySettings.youtube, env, comms) + initOverlays(overlaySettings.youtube, env, comms); } else if (serpProxyEnabled) { - comms.serpProxy() + comms.serpProxy(); } } - load (args) { - super.load(args) + load(args) { + super.load(args); } } @@ -119,4 +118,4 @@ export default class DuckPlayerFeature extends ContentFeature { */ // for docs generation -export { DuckPlayerOverlayMessages, OpenInDuckPlayerMsg, Pixel } +export { DuckPlayerOverlayMessages, OpenInDuckPlayerMsg, Pixel }; diff --git a/injected/src/features/duckplayer/components/ddg-video-overlay-mobile.js b/injected/src/features/duckplayer/components/ddg-video-overlay-mobile.js index 4f11f88be..daaf51c61 100644 --- a/injected/src/features/duckplayer/components/ddg-video-overlay-mobile.js +++ b/injected/src/features/duckplayer/components/ddg-video-overlay-mobile.js @@ -1,7 +1,7 @@ -import mobilecss from '../assets/mobile-video-overlay.css' -import dax from '../assets/dax.svg' -import info from '../assets/info.svg' -import { createPolicy, html, trustedUnsafe } from '../../../dom-utils.js' +import mobilecss from '../assets/mobile-video-overlay.css'; +import dax from '../assets/dax.svg'; +import info from '../assets/info.svg'; +import { createPolicy, html, trustedUnsafe } from '../../../dom-utils.js'; /** * @typedef {ReturnType} TextVariants @@ -13,42 +13,42 @@ import { createPolicy, html, trustedUnsafe } from '../../../dom-utils.js' * over the YouTube player */ export class DDGVideoOverlayMobile extends HTMLElement { - static CUSTOM_TAG_NAME = 'ddg-video-overlay-mobile' - static OPEN_INFO = 'open-info' - static OPT_IN = 'opt-in' - static OPT_OUT = 'opt-out' + static CUSTOM_TAG_NAME = 'ddg-video-overlay-mobile'; + static OPEN_INFO = 'open-info'; + static OPT_IN = 'opt-in'; + static OPT_OUT = 'opt-out'; - policy = createPolicy() + policy = createPolicy(); /** @type {boolean} */ - testMode = false + testMode = false; /** @type {Text | null} */ - text = null + text = null; - connectedCallback () { - this.createMarkupAndStyles() + connectedCallback() { + this.createMarkupAndStyles(); } - createMarkupAndStyles () { - const shadow = this.attachShadow({ mode: this.testMode ? 'open' : 'closed' }) - const style = document.createElement('style') - style.innerText = mobilecss - const overlayElement = document.createElement('div') - const content = this.mobileHtml() - overlayElement.innerHTML = this.policy.createHTML(content) - shadow.append(style, overlayElement) - this.setupEventHandlers(overlayElement) + createMarkupAndStyles() { + const shadow = this.attachShadow({ mode: this.testMode ? 'open' : 'closed' }); + const style = document.createElement('style'); + style.innerText = mobilecss; + const overlayElement = document.createElement('div'); + const content = this.mobileHtml(); + overlayElement.innerHTML = this.policy.createHTML(content); + shadow.append(style, overlayElement); + this.setupEventHandlers(overlayElement); } /** * @returns {string} */ - mobileHtml () { + mobileHtml() { if (!this.text) { - console.warn('missing `text`. Please assign before rendering') - return '' + console.warn('missing `text`. Please assign before rendering'); + return ''; } - const svgIcon = trustedUnsafe(dax) - const infoIcon = trustedUnsafe(info) + const svgIcon = trustedUnsafe(dax); + const infoIcon = trustedUnsafe(info); return html`
@@ -56,24 +56,18 @@ export class DDGVideoOverlayMobile extends HTMLElement {
${this.text.title}
- -
-
- ${this.text.subtitle} +
+
${this.text.subtitle}
${this.text.buttonOpen}
- - ${this.text.rememberLabel} - + ${this.text.rememberLabel} - + @@ -82,52 +76,49 @@ export class DDGVideoOverlayMobile extends HTMLElement {
- `.toString() + `.toString(); } /** * @param {HTMLElement} containerElement */ - setupEventHandlers (containerElement) { - const switchElem = containerElement.querySelector('[role=switch]') - const infoButton = containerElement.querySelector('.button--info') - const remember = containerElement.querySelector('input[name="ddg-remember"]') - const cancelElement = containerElement.querySelector('.ddg-vpo-cancel') - const watchInPlayer = containerElement.querySelector('.ddg-vpo-open') + setupEventHandlers(containerElement) { + const switchElem = containerElement.querySelector('[role=switch]'); + const infoButton = containerElement.querySelector('.button--info'); + const remember = containerElement.querySelector('input[name="ddg-remember"]'); + const cancelElement = containerElement.querySelector('.ddg-vpo-cancel'); + const watchInPlayer = containerElement.querySelector('.ddg-vpo-open'); - if (!infoButton || - !cancelElement || - !watchInPlayer || - !switchElem || - !(remember instanceof HTMLInputElement)) return console.warn('missing elements') + if (!infoButton || !cancelElement || !watchInPlayer || !switchElem || !(remember instanceof HTMLInputElement)) + return console.warn('missing elements'); infoButton.addEventListener('click', () => { - this.dispatchEvent(new Event(DDGVideoOverlayMobile.OPEN_INFO)) - }) + this.dispatchEvent(new Event(DDGVideoOverlayMobile.OPEN_INFO)); + }); switchElem.addEventListener('pointerdown', () => { - const current = switchElem.getAttribute('aria-checked') + const current = switchElem.getAttribute('aria-checked'); if (current === 'false') { - switchElem.setAttribute('aria-checked', 'true') - remember.checked = true + switchElem.setAttribute('aria-checked', 'true'); + remember.checked = true; } else { - switchElem.setAttribute('aria-checked', 'false') - remember.checked = false + switchElem.setAttribute('aria-checked', 'false'); + remember.checked = false; } - }) + }); cancelElement.addEventListener('click', (e) => { - if (!e.isTrusted) return - e.preventDefault() - e.stopImmediatePropagation() - this.dispatchEvent(new CustomEvent(DDGVideoOverlayMobile.OPT_OUT, { detail: { remember: remember.checked } })) - }) + if (!e.isTrusted) return; + e.preventDefault(); + e.stopImmediatePropagation(); + this.dispatchEvent(new CustomEvent(DDGVideoOverlayMobile.OPT_OUT, { detail: { remember: remember.checked } })); + }); watchInPlayer.addEventListener('click', (e) => { - if (!e.isTrusted) return - e.preventDefault() - e.stopImmediatePropagation() - this.dispatchEvent(new CustomEvent(DDGVideoOverlayMobile.OPT_IN, { detail: { remember: remember.checked } })) - }) + if (!e.isTrusted) return; + e.preventDefault(); + e.stopImmediatePropagation(); + this.dispatchEvent(new CustomEvent(DDGVideoOverlayMobile.OPT_IN, { detail: { remember: remember.checked } })); + }); } } diff --git a/injected/src/features/duckplayer/components/ddg-video-overlay.js b/injected/src/features/duckplayer/components/ddg-video-overlay.js index edf2515e9..9712b637f 100644 --- a/injected/src/features/duckplayer/components/ddg-video-overlay.js +++ b/injected/src/features/duckplayer/components/ddg-video-overlay.js @@ -1,18 +1,18 @@ -import css from '../assets/video-overlay.css' -import dax from '../assets/dax.svg' -import { overlayCopyVariants } from '../text.js' -import { appendImageAsBackground } from '../util.js' -import { VideoOverlay } from '../video-overlay.js' -import { createPolicy, html, trustedUnsafe } from '../../../dom-utils.js' +import css from '../assets/video-overlay.css'; +import dax from '../assets/dax.svg'; +import { overlayCopyVariants } from '../text.js'; +import { appendImageAsBackground } from '../util.js'; +import { VideoOverlay } from '../video-overlay.js'; +import { createPolicy, html, trustedUnsafe } from '../../../dom-utils.js'; /** * The custom element that we use to present our UI elements * over the YouTube player */ export class DDGVideoOverlay extends HTMLElement { - policy = createPolicy() + policy = createPolicy(); - static CUSTOM_TAG_NAME = 'ddg-video-overlay' + static CUSTOM_TAG_NAME = 'ddg-video-overlay'; /** * @param {object} options * @param {import("../overlays.js").Environment} options.environment @@ -20,124 +20,120 @@ export class DDGVideoOverlay extends HTMLElement { * @param {import("../../duck-player.js").UISettings} options.ui * @param {VideoOverlay} options.manager */ - constructor ({ environment, params, ui, manager }) { - super() - if (!(manager instanceof VideoOverlay)) throw new Error('invalid arguments') - this.environment = environment - this.ui = ui - this.params = params - this.manager = manager + constructor({ environment, params, ui, manager }) { + super(); + if (!(manager instanceof VideoOverlay)) throw new Error('invalid arguments'); + this.environment = environment; + this.ui = ui; + this.params = params; + this.manager = manager; /** * Create the shadow root, closed to prevent any outside observers * @type {ShadowRoot} */ - const shadow = this.attachShadow({ mode: this.environment.isTestMode() ? 'open' : 'closed' }) + const shadow = this.attachShadow({ mode: this.environment.isTestMode() ? 'open' : 'closed' }); /** * Add our styles * @type {HTMLStyleElement} */ - const style = document.createElement('style') - style.innerText = css + const style = document.createElement('style'); + style.innerText = css; /** * Create the overlay * @type {HTMLDivElement} */ - const overlay = this.createOverlay() + const overlay = this.createOverlay(); /** * Append both to the shadow root */ - shadow.appendChild(overlay) - shadow.appendChild(style) + shadow.appendChild(overlay); + shadow.appendChild(style); } /** * @returns {HTMLDivElement} */ - createOverlay () { - const overlayCopy = overlayCopyVariants.default - const overlayElement = document.createElement('div') - overlayElement.classList.add('ddg-video-player-overlay') - const svgIcon = trustedUnsafe(dax) + createOverlay() { + const overlayCopy = overlayCopyVariants.default; + const overlayElement = document.createElement('div'); + overlayElement.classList.add('ddg-video-player-overlay'); + const svgIcon = trustedUnsafe(dax); const safeString = html`
${svgIcon}
${overlayCopy.title}
-
- ${overlayCopy.subtitle} -
+
${overlayCopy.subtitle}
${overlayCopy.buttonOpen}
- +
- `.toString() + `.toString(); - overlayElement.innerHTML = this.policy.createHTML(safeString) + overlayElement.innerHTML = this.policy.createHTML(safeString); /** * Set the link * @type {string} */ - const href = this.params.toPrivatePlayerUrl() - overlayElement.querySelector('.ddg-vpo-open')?.setAttribute('href', href) + const href = this.params.toPrivatePlayerUrl(); + overlayElement.querySelector('.ddg-vpo-open')?.setAttribute('href', href); /** * Add thumbnail */ - this.appendThumbnail(overlayElement, this.params.id) + this.appendThumbnail(overlayElement, this.params.id); /** * Setup the click handlers */ - this.setupButtonsInsideOverlay(overlayElement, this.params) + this.setupButtonsInsideOverlay(overlayElement, this.params); - return overlayElement + return overlayElement; } /** * @param {HTMLElement} overlayElement * @param {string} videoId */ - appendThumbnail (overlayElement, videoId) { - const imageUrl = this.environment.getLargeThumbnailSrc(videoId) - appendImageAsBackground(overlayElement, '.ddg-vpo-bg', imageUrl) + appendThumbnail(overlayElement, videoId) { + const imageUrl = this.environment.getLargeThumbnailSrc(videoId); + appendImageAsBackground(overlayElement, '.ddg-vpo-bg', imageUrl); } /** * @param {HTMLElement} containerElement * @param {import("../util").VideoParams} params */ - setupButtonsInsideOverlay (containerElement, params) { - const cancelElement = containerElement.querySelector('.ddg-vpo-cancel') - const watchInPlayer = containerElement.querySelector('.ddg-vpo-open') - if (!cancelElement) return console.warn('Could not access .ddg-vpo-cancel') - if (!watchInPlayer) return console.warn('Could not access .ddg-vpo-open') + setupButtonsInsideOverlay(containerElement, params) { + const cancelElement = containerElement.querySelector('.ddg-vpo-cancel'); + const watchInPlayer = containerElement.querySelector('.ddg-vpo-open'); + if (!cancelElement) return console.warn('Could not access .ddg-vpo-cancel'); + if (!watchInPlayer) return console.warn('Could not access .ddg-vpo-open'); const optOutHandler = (e) => { if (e.isTrusted) { - const remember = containerElement.querySelector('input[name="ddg-remember"]') - if (!(remember instanceof HTMLInputElement)) throw new Error('cannot find our input') - this.manager.userOptOut(remember.checked, params) + const remember = containerElement.querySelector('input[name="ddg-remember"]'); + if (!(remember instanceof HTMLInputElement)) throw new Error('cannot find our input'); + this.manager.userOptOut(remember.checked, params); } - } + }; const watchInPlayerHandler = (e) => { if (e.isTrusted) { - e.preventDefault() - const remember = containerElement.querySelector('input[name="ddg-remember"]') - if (!(remember instanceof HTMLInputElement)) throw new Error('cannot find our input') - this.manager.userOptIn(remember.checked, params) + e.preventDefault(); + const remember = containerElement.querySelector('input[name="ddg-remember"]'); + if (!(remember instanceof HTMLInputElement)) throw new Error('cannot find our input'); + this.manager.userOptIn(remember.checked, params); } - } - cancelElement.addEventListener('click', optOutHandler) - watchInPlayer.addEventListener('click', watchInPlayerHandler) + }; + cancelElement.addEventListener('click', optOutHandler); + watchInPlayer.addEventListener('click', watchInPlayerHandler); } } diff --git a/injected/src/features/duckplayer/components/index.js b/injected/src/features/duckplayer/components/index.js index baa51e4ab..730b22e9e 100644 --- a/injected/src/features/duckplayer/components/index.js +++ b/injected/src/features/duckplayer/components/index.js @@ -1,17 +1,17 @@ -import { DDGVideoOverlay } from './ddg-video-overlay.js' -import { customElementsDefine, customElementsGet } from '../../../captured-globals.js' -import { DDGVideoOverlayMobile } from './ddg-video-overlay-mobile.js' +import { DDGVideoOverlay } from './ddg-video-overlay.js'; +import { customElementsDefine, customElementsGet } from '../../../captured-globals.js'; +import { DDGVideoOverlayMobile } from './ddg-video-overlay-mobile.js'; /** * Register custom elements in this wrapper function to be called only when we need to * and also to allow remote-config later if needed. * */ -export function registerCustomElements () { +export function registerCustomElements() { if (!customElementsGet(DDGVideoOverlay.CUSTOM_TAG_NAME)) { - customElementsDefine(DDGVideoOverlay.CUSTOM_TAG_NAME, DDGVideoOverlay) + customElementsDefine(DDGVideoOverlay.CUSTOM_TAG_NAME, DDGVideoOverlay); } if (!customElementsGet(DDGVideoOverlayMobile.CUSTOM_TAG_NAME)) { - customElementsDefine(DDGVideoOverlayMobile.CUSTOM_TAG_NAME, DDGVideoOverlayMobile) + customElementsDefine(DDGVideoOverlayMobile.CUSTOM_TAG_NAME, DDGVideoOverlayMobile); } } diff --git a/injected/src/features/duckplayer/constants.js b/injected/src/features/duckplayer/constants.js index c45942683..29e40718c 100644 --- a/injected/src/features/duckplayer/constants.js +++ b/injected/src/features/duckplayer/constants.js @@ -1,10 +1,10 @@ -export const MSG_NAME_INITIAL_SETUP = 'initialSetup' -export const MSG_NAME_SET_VALUES = 'setUserValues' -export const MSG_NAME_READ_VALUES = 'getUserValues' -export const MSG_NAME_READ_VALUES_SERP = 'readUserValues' -export const MSG_NAME_OPEN_PLAYER = 'openDuckPlayer' -export const MSG_NAME_OPEN_INFO = 'openInfo' -export const MSG_NAME_PUSH_DATA = 'onUserValuesChanged' -export const MSG_NAME_PIXEL = 'sendDuckPlayerPixel' -export const MSG_NAME_PROXY_INCOMING = 'ddg-serp-yt' -export const MSG_NAME_PROXY_RESPONSE = 'ddg-serp-yt-response' +export const MSG_NAME_INITIAL_SETUP = 'initialSetup'; +export const MSG_NAME_SET_VALUES = 'setUserValues'; +export const MSG_NAME_READ_VALUES = 'getUserValues'; +export const MSG_NAME_READ_VALUES_SERP = 'readUserValues'; +export const MSG_NAME_OPEN_PLAYER = 'openDuckPlayer'; +export const MSG_NAME_OPEN_INFO = 'openInfo'; +export const MSG_NAME_PUSH_DATA = 'onUserValuesChanged'; +export const MSG_NAME_PIXEL = 'sendDuckPlayerPixel'; +export const MSG_NAME_PROXY_INCOMING = 'ddg-serp-yt'; +export const MSG_NAME_PROXY_RESPONSE = 'ddg-serp-yt-response'; diff --git a/injected/src/features/duckplayer/icon-overlay.js b/injected/src/features/duckplayer/icon-overlay.js index 33787f344..5273a6863 100644 --- a/injected/src/features/duckplayer/icon-overlay.js +++ b/injected/src/features/duckplayer/icon-overlay.js @@ -1,28 +1,28 @@ -import css from './assets/styles.css' -import { SideEffects, VideoParams } from './util.js' -import dax from './assets/dax.svg' -import { i18n } from './text.js' -import { createPolicy, html, trustedUnsafe } from '../../dom-utils.js' +import css from './assets/styles.css'; +import { SideEffects, VideoParams } from './util.js'; +import dax from './assets/dax.svg'; +import { i18n } from './text.js'; +import { createPolicy, html, trustedUnsafe } from '../../dom-utils.js'; export class IconOverlay { - sideEffects = new SideEffects() - policy = createPolicy() + sideEffects = new SideEffects(); + policy = createPolicy(); /** @type {HTMLElement | null} */ - element = null + element = null; /** * Special class used for the overlay hover. For hovering, we use a * single element and move it around to the hovered video element. */ - HOVER_CLASS = 'ddg-overlay-hover' - OVERLAY_CLASS = 'ddg-overlay' + HOVER_CLASS = 'ddg-overlay-hover'; + OVERLAY_CLASS = 'ddg-overlay'; - CSS_OVERLAY_MARGIN_TOP = 5 - CSS_OVERLAY_HEIGHT = 32 + CSS_OVERLAY_MARGIN_TOP = 5; + CSS_OVERLAY_HEIGHT = 32; /** @type {HTMLElement | null} */ - currentVideoElement = null - hoverOverlayVisible = false + currentVideoElement = null; + hoverOverlayVisible = false; /** * Creates an Icon Overlay. @@ -31,70 +31,64 @@ export class IconOverlay { * @param {string} [extraClass] - whether to add any extra classes, such as hover * @returns {HTMLElement} */ - create (size, href, extraClass) { - const overlayElement = document.createElement('div') - - overlayElement.setAttribute('class', 'ddg-overlay' + (extraClass ? ' ' + extraClass : '')) - overlayElement.setAttribute('data-size', size) - const svgIcon = trustedUnsafe(dax) - const safeString = html` - -
- ${svgIcon} -
-
-
- ${i18n.t('playText')} -
-
-
`.toString() - - overlayElement.innerHTML = this.policy.createHTML(safeString) - - overlayElement.querySelector('a.ddg-play-privately')?.setAttribute('href', href) - return overlayElement + create(size, href, extraClass) { + const overlayElement = document.createElement('div'); + + overlayElement.setAttribute('class', 'ddg-overlay' + (extraClass ? ' ' + extraClass : '')); + overlayElement.setAttribute('data-size', size); + const svgIcon = trustedUnsafe(dax); + const safeString = html` +
${svgIcon}
+
+
${i18n.t('playText')}
+
+
`.toString(); + + overlayElement.innerHTML = this.policy.createHTML(safeString); + + overlayElement.querySelector('a.ddg-play-privately')?.setAttribute('href', href); + return overlayElement; } /** * Util to return the hover overlay * @returns {HTMLElement | null} */ - getHoverOverlay () { - return document.querySelector('.' + this.HOVER_CLASS) + getHoverOverlay() { + return document.querySelector('.' + this.HOVER_CLASS); } /** * Moves the hover overlay to a specified videoElement * @param {HTMLElement} videoElement - which element to move it to */ - moveHoverOverlayToVideoElement (videoElement) { - const overlay = this.getHoverOverlay() + moveHoverOverlayToVideoElement(videoElement) { + const overlay = this.getHoverOverlay(); if (overlay === null || this.videoScrolledOutOfViewInPlaylist(videoElement)) { - return + return; } - const videoElementOffset = this.getElementOffset(videoElement) + const videoElementOffset = this.getElementOffset(videoElement); - overlay.setAttribute('style', '' + - 'top: ' + videoElementOffset.top + 'px;' + - 'left: ' + videoElementOffset.left + 'px;' + - 'display:block;' - ) + overlay.setAttribute( + 'style', + '' + 'top: ' + videoElementOffset.top + 'px;' + 'left: ' + videoElementOffset.left + 'px;' + 'display:block;', + ); - overlay.setAttribute('data-size', 'fixed ' + this.getThumbnailSize(videoElement)) + overlay.setAttribute('data-size', 'fixed ' + this.getThumbnailSize(videoElement)); - const href = videoElement.getAttribute('href') + const href = videoElement.getAttribute('href'); if (href) { - const privateUrl = VideoParams.fromPathname(href)?.toPrivatePlayerUrl() + const privateUrl = VideoParams.fromPathname(href)?.toPrivatePlayerUrl(); if (overlay && privateUrl) { - overlay.querySelector('a')?.setAttribute('href', privateUrl) + overlay.querySelector('a')?.setAttribute('href', privateUrl); } } - this.hoverOverlayVisible = true - this.currentVideoElement = videoElement + this.hoverOverlayVisible = true; + this.currentVideoElement = videoElement; } /** @@ -103,22 +97,22 @@ export class IconOverlay { * @param {HTMLElement} videoElement * @returns {boolean} */ - videoScrolledOutOfViewInPlaylist (videoElement) { - const inPlaylist = videoElement.closest('#items.playlist-items') + videoScrolledOutOfViewInPlaylist(videoElement) { + const inPlaylist = videoElement.closest('#items.playlist-items'); if (inPlaylist) { - const video = videoElement.getBoundingClientRect() - const playlist = inPlaylist.getBoundingClientRect() + const video = videoElement.getBoundingClientRect(); + const playlist = inPlaylist.getBoundingClientRect(); - const videoOutsideTop = (video.top + this.CSS_OVERLAY_MARGIN_TOP) < playlist.top - const videoOutsideBottom = ((video.top + this.CSS_OVERLAY_HEIGHT + this.CSS_OVERLAY_MARGIN_TOP) > playlist.bottom) + const videoOutsideTop = video.top + this.CSS_OVERLAY_MARGIN_TOP < playlist.top; + const videoOutsideBottom = video.top + this.CSS_OVERLAY_HEIGHT + this.CSS_OVERLAY_MARGIN_TOP > playlist.bottom; if (videoOutsideTop || videoOutsideBottom) { - return true + return true; } } - return false + return false; } /** @@ -126,32 +120,32 @@ export class IconOverlay { * @param {HTMLElement} el * @returns {Object} */ - getElementOffset (el) { - const box = el.getBoundingClientRect() - const docElem = document.documentElement + getElementOffset(el) { + const box = el.getBoundingClientRect(); + const docElem = document.documentElement; return { top: box.top + window.pageYOffset - docElem.clientTop, - left: box.left + window.pageXOffset - docElem.clientLeft - } + left: box.left + window.pageXOffset - docElem.clientLeft, + }; } /** * Hides the hover overlay element, but only if mouse pointer is outside of the hover overlay element */ - hideHoverOverlay (event, force) { - const overlay = this.getHoverOverlay() + hideHoverOverlay(event, force) { + const overlay = this.getHoverOverlay(); - const toElement = event.toElement + const toElement = event.toElement; if (overlay) { // Prevent hiding overlay if mouseleave is triggered by user is actually hovering it and that // triggered the mouseleave event if (toElement === overlay || overlay.contains(toElement) || force) { - return + return; } - this.hideOverlay(overlay) - this.hoverOverlayVisible = false + this.hideOverlay(overlay); + this.hoverOverlayVisible = false; } } @@ -159,8 +153,8 @@ export class IconOverlay { * Util for hiding an overlay * @param {HTMLElement} overlay */ - hideOverlay (overlay) { - overlay.setAttribute('style', 'display:none;') + hideOverlay(overlay) { + overlay.setAttribute('style', 'display:none;'); } /** @@ -170,40 +164,40 @@ export class IconOverlay { * inside a video thumbnail when hovering the overlay. Nice. * @param {(href: string) => void} onClick */ - appendHoverOverlay (onClick) { + appendHoverOverlay(onClick) { this.sideEffects.add('Adding the re-usable overlay to the page ', () => { // add the CSS to the head - const cleanUpCSS = this.loadCSS() + const cleanUpCSS = this.loadCSS(); // create and append the element - const element = this.create('fixed', '', this.HOVER_CLASS) - document.body.appendChild(element) + const element = this.create('fixed', '', this.HOVER_CLASS); + document.body.appendChild(element); - this.addClickHandler(element, onClick) + this.addClickHandler(element, onClick); return () => { - element.remove() - cleanUpCSS() - } - }) + element.remove(); + cleanUpCSS(); + }; + }); } - loadCSS () { + loadCSS() { // add the CSS to the head - const id = '__ddg__icon' - const style = document.head.querySelector(`#${id}`) + const id = '__ddg__icon'; + const style = document.head.querySelector(`#${id}`); if (!style) { - const style = document.createElement('style') - style.id = id - style.textContent = css - document.head.appendChild(style) + const style = document.createElement('style'); + style.id = id; + style.textContent = css; + document.head.appendChild(style); } return () => { - const style = document.head.querySelector(`#${id}`) + const style = document.head.querySelector(`#${id}`); if (style) { - document.head.removeChild(style) + document.head.removeChild(style); } - } + }; } /** @@ -211,45 +205,46 @@ export class IconOverlay { * @param {string} href * @param {(href: string) => void} onClick */ - appendSmallVideoOverlay (container, href, onClick) { + appendSmallVideoOverlay(container, href, onClick) { this.sideEffects.add('Adding a small overlay for the video player', () => { // add the CSS to the head - const cleanUpCSS = this.loadCSS() + const cleanUpCSS = this.loadCSS(); - const element = this.create('video-player', href, 'hidden') + const element = this.create('video-player', href, 'hidden'); - this.addClickHandler(element, onClick) + this.addClickHandler(element, onClick); - container.appendChild(element) - element.classList.remove('hidden') + container.appendChild(element); + element.classList.remove('hidden'); return () => { - element?.remove() - cleanUpCSS() - } - }) + element?.remove(); + cleanUpCSS(); + }; + }); } - getThumbnailSize (videoElement) { - const imagesByArea = {} + getThumbnailSize(videoElement) { + const imagesByArea = {}; - Array.from(videoElement.querySelectorAll('img')).forEach(image => { - imagesByArea[(image.offsetWidth * image.offsetHeight)] = image - }) + Array.from(videoElement.querySelectorAll('img')).forEach((image) => { + imagesByArea[image.offsetWidth * image.offsetHeight] = image; + }); - const largestImage = Math.max.apply(this, Object.keys(imagesByArea).map(Number)) + const largestImage = Math.max.apply(this, Object.keys(imagesByArea).map(Number)); const getSizeType = (width, height) => { - if (width < (123 + 10)) { // match CSS: width of expanded overlay + twice the left margin. - return 'small' + if (width < 123 + 10) { + // match CSS: width of expanded overlay + twice the left margin. + return 'small'; } else if (width < 300 && height < 175) { - return 'medium' + return 'medium'; } else { - return 'large' + return 'large'; } - } + }; - return getSizeType(imagesByArea[largestImage].offsetWidth, imagesByArea[largestImage].offsetHeight) + return getSizeType(imagesByArea[largestImage].offsetWidth, imagesByArea[largestImage].offsetHeight); } /** @@ -259,19 +254,19 @@ export class IconOverlay { * @param {HTMLElement} element - the wrapping div * @param {(href: string) => void} callback - the function to execute following a click */ - addClickHandler (element, callback) { + addClickHandler(element, callback) { element.addEventListener('click', (event) => { - event.preventDefault() - event.stopImmediatePropagation() - const link = /** @type {HTMLElement} */(event.target).closest('a') - const href = link?.getAttribute('href') + event.preventDefault(); + event.stopImmediatePropagation(); + const link = /** @type {HTMLElement} */ (event.target).closest('a'); + const href = link?.getAttribute('href'); if (href) { - callback(href) + callback(href); } - }) + }); } - destroy () { - this.sideEffects.destroy() + destroy() { + this.sideEffects.destroy(); } } diff --git a/injected/src/features/duckplayer/overlay-messages.js b/injected/src/features/duckplayer/overlay-messages.js index 76613aa92..a394f6e25 100644 --- a/injected/src/features/duckplayer/overlay-messages.js +++ b/injected/src/features/duckplayer/overlay-messages.js @@ -1,5 +1,5 @@ /* eslint-disable promise/prefer-await-to-then */ -import * as constants from './constants.js' +import * as constants from './constants.js'; /** * @typedef {import("@duckduckgo/messaging").Messaging} Messaging @@ -15,28 +15,28 @@ export class DuckPlayerOverlayMessages { * @param {import('./overlays.js').Environment} environment * @internal */ - constructor (messaging, environment) { + constructor(messaging, environment) { /** * @internal */ - this.messaging = messaging - this.environment = environment + this.messaging = messaging; + this.environment = environment; } /** * @returns {Promise} */ - initialSetup () { + initialSetup() { if (this.environment.isIntegrationMode()) { return Promise.resolve({ userValues: { overlayInteracted: false, - privatePlayerMode: { alwaysAsk: {} } + privatePlayerMode: { alwaysAsk: {} }, }, - ui: {} - }) + ui: {}, + }); } - return this.messaging.request(constants.MSG_NAME_INITIAL_SETUP) + return this.messaging.request(constants.MSG_NAME_INITIAL_SETUP); } /** @@ -44,25 +44,25 @@ export class DuckPlayerOverlayMessages { * @param {import("../duck-player.js").UserValues} userValues * @returns {Promise} */ - setUserValues (userValues) { - return this.messaging.request(constants.MSG_NAME_SET_VALUES, userValues) + setUserValues(userValues) { + return this.messaging.request(constants.MSG_NAME_SET_VALUES, userValues); } /** * @returns {Promise} */ - getUserValues () { - return this.messaging.request(constants.MSG_NAME_READ_VALUES, {}) + getUserValues() { + return this.messaging.request(constants.MSG_NAME_READ_VALUES, {}); } /** * @param {Pixel} pixel */ - sendPixel (pixel) { + sendPixel(pixel) { this.messaging.notify(constants.MSG_NAME_PIXEL, { pixelName: pixel.name(), - params: pixel.params() - }) + params: pixel.params(), + }); } /** @@ -70,72 +70,74 @@ export class DuckPlayerOverlayMessages { * See {@link OpenInDuckPlayerMsg} for params * @param {OpenInDuckPlayerMsg} params */ - openDuckPlayer (params) { - return this.messaging.notify(constants.MSG_NAME_OPEN_PLAYER, params) + openDuckPlayer(params) { + return this.messaging.notify(constants.MSG_NAME_OPEN_PLAYER, params); } /** * This is sent when the user wants to open Duck Player. */ - openInfo () { - return this.messaging.notify(constants.MSG_NAME_OPEN_INFO) + openInfo() { + return this.messaging.notify(constants.MSG_NAME_OPEN_INFO); } /** * Get notification when preferences/state changed * @param {(userValues: import("../duck-player.js").UserValues) => void} cb */ - onUserValuesChanged (cb) { - return this.messaging.subscribe('onUserValuesChanged', cb) + onUserValuesChanged(cb) { + return this.messaging.subscribe('onUserValuesChanged', cb); } /** * Get notification when ui settings changed * @param {(userValues: import("../duck-player.js").UISettings) => void} cb */ - onUIValuesChanged (cb) { - return this.messaging.subscribe('onUIValuesChanged', cb) + onUIValuesChanged(cb) { + return this.messaging.subscribe('onUIValuesChanged', cb); } /** * This allows our SERP to interact with Duck Player settings. */ - serpProxy () { - function respond (kind, data) { - window.dispatchEvent(new CustomEvent(constants.MSG_NAME_PROXY_RESPONSE, { - detail: { kind, data }, - composed: true, - bubbles: true - })) + serpProxy() { + function respond(kind, data) { + window.dispatchEvent( + new CustomEvent(constants.MSG_NAME_PROXY_RESPONSE, { + detail: { kind, data }, + composed: true, + bubbles: true, + }), + ); } // listen for setting and forward to the SERP window this.onUserValuesChanged((values) => { - respond(constants.MSG_NAME_PUSH_DATA, values) - }) + respond(constants.MSG_NAME_PUSH_DATA, values); + }); // accept messages from the SERP and forward them to native window.addEventListener(constants.MSG_NAME_PROXY_INCOMING, (evt) => { try { - assertCustomEvent(evt) + assertCustomEvent(evt); if (evt.detail.kind === constants.MSG_NAME_SET_VALUES) { return this.setUserValues(evt.detail.data) - .then(updated => respond(constants.MSG_NAME_PUSH_DATA, updated)) - .catch(console.error) + .then((updated) => respond(constants.MSG_NAME_PUSH_DATA, updated)) + .catch(console.error); } if (evt.detail.kind === constants.MSG_NAME_READ_VALUES_SERP) { return this.getUserValues() - .then(updated => respond(constants.MSG_NAME_PUSH_DATA, updated)) - .catch(console.error) + .then((updated) => respond(constants.MSG_NAME_PUSH_DATA, updated)) + .catch(console.error); } if (evt.detail.kind === constants.MSG_NAME_OPEN_INFO) { - return this.openInfo() + return this.openInfo(); } - console.warn('unhandled event', evt) + console.warn('unhandled event', evt); } catch (e) { - console.warn('cannot handle this message', e) + console.warn('cannot handle this message', e); } - }) + }); } } @@ -143,9 +145,9 @@ export class DuckPlayerOverlayMessages { * @param {any} event * @returns {asserts event is CustomEvent<{kind: string, data: any}>} */ -function assertCustomEvent (event) { - if (!('detail' in event)) throw new Error('none-custom event') - if (typeof event.detail.kind !== 'string') throw new Error('custom event requires detail.kind to be a string') +function assertCustomEvent(event) { + if (!('detail' in event)) throw new Error('none-custom event'); + if (typeof event.detail.kind !== 'string') throw new Error('custom event requires detail.kind to be a string'); } export class Pixel { @@ -156,23 +158,26 @@ export class Pixel { * | {name: "play.use.thumbnail"} * | {name: "play.do_not_use", remember: "0" | "1"}} input */ - constructor (input) { - this.input = input + constructor(input) { + this.input = input; } - name () { - return this.input.name + name() { + return this.input.name; } - params () { + params() { switch (this.input.name) { - case 'overlay': return {} - case 'play.use.thumbnail': return {} - case 'play.use': - case 'play.do_not_use': { - return { remember: this.input.remember } - } - default: throw new Error('unreachable') + case 'overlay': + return {}; + case 'play.use.thumbnail': + return {}; + case 'play.use': + case 'play.do_not_use': { + return { remember: this.input.remember }; + } + default: + throw new Error('unreachable'); } } } @@ -182,7 +187,7 @@ export class OpenInDuckPlayerMsg { * @param {object} params * @param {string} params.href */ - constructor (params) { - this.href = params.href + constructor(params) { + this.href = params.href; } } diff --git a/injected/src/features/duckplayer/overlays.js b/injected/src/features/duckplayer/overlays.js index 6d9866bd5..4fc4909ff 100644 --- a/injected/src/features/duckplayer/overlays.js +++ b/injected/src/features/duckplayer/overlays.js @@ -1,8 +1,8 @@ -import { DomState } from './util.js' -import { ClickInterception, Thumbnails } from './thumbnails.js' -import { VideoOverlay } from './video-overlay.js' -import { registerCustomElements } from './components/index.js' -import strings from '../../../../build/locales/duckplayer-locales.js' +import { DomState } from './util.js'; +import { ClickInterception, Thumbnails } from './thumbnails.js'; +import { VideoOverlay } from './video-overlay.js'; +import { registerCustomElements } from './components/index.js'; +import strings from '../../../../build/locales/duckplayer-locales.js'; /** * @typedef {object} OverlayOptions @@ -18,101 +18,101 @@ import strings from '../../../../build/locales/duckplayer-locales.js' * @param {import("./overlays.js").Environment} environment - methods to read environment-sensitive things like the current URL etc * @param {import("./overlay-messages.js").DuckPlayerOverlayMessages} messages - methods to communicate with a native backend */ -export async function initOverlays (settings, environment, messages) { +export async function initOverlays(settings, environment, messages) { // bind early to attach all listeners - const domState = new DomState() + const domState = new DomState(); /** @type {import("../duck-player.js").OverlaysInitialSettings} */ - let initialSetup + let initialSetup; try { - initialSetup = await messages.initialSetup() + initialSetup = await messages.initialSetup(); } catch (e) { - console.error(e) - return + console.error(e); + return; } if (!initialSetup) { - console.error('cannot continue without user settings') - return + console.error('cannot continue without user settings'); + return; } - let { userValues, ui } = initialSetup + let { userValues, ui } = initialSetup; /** * Create the instance - this might fail if settings or user preferences prevent it * @type {Thumbnails|ClickInterception|null} */ - let thumbnails = thumbnailsFeatureFromOptions({ userValues, settings, messages, environment, ui }) - let videoOverlays = videoOverlaysFeatureFromSettings({ userValues, settings, messages, environment, ui }) + let thumbnails = thumbnailsFeatureFromOptions({ userValues, settings, messages, environment, ui }); + let videoOverlays = videoOverlaysFeatureFromSettings({ userValues, settings, messages, environment, ui }); if (thumbnails || videoOverlays) { if (videoOverlays) { - registerCustomElements() - videoOverlays?.init('page-load') + registerCustomElements(); + videoOverlays?.init('page-load'); } domState.onLoaded(() => { // start initially - thumbnails?.init() + thumbnails?.init(); // now add video overlay specific stuff if (videoOverlays) { // there was an issue capturing history.pushState, so just falling back to - let prev = globalThis.location.href + let prev = globalThis.location.href; setInterval(() => { if (globalThis.location.href !== prev) { - videoOverlays?.init('href-changed') + videoOverlays?.init('href-changed'); } - prev = globalThis.location.href - }, 500) + prev = globalThis.location.href; + }, 500); } - }) + }); } - function update () { - thumbnails?.destroy() - videoOverlays?.destroy() + function update() { + thumbnails?.destroy(); + videoOverlays?.destroy(); // re-create thumbs - thumbnails = thumbnailsFeatureFromOptions({ userValues, settings, messages, environment, ui }) - thumbnails?.init() + thumbnails = thumbnailsFeatureFromOptions({ userValues, settings, messages, environment, ui }); + thumbnails?.init(); // re-create video overlay - videoOverlays = videoOverlaysFeatureFromSettings({ userValues, settings, messages, environment, ui }) - videoOverlays?.init('preferences-changed') + videoOverlays = videoOverlaysFeatureFromSettings({ userValues, settings, messages, environment, ui }); + videoOverlays?.init('preferences-changed'); } /** * Continue to listen for updated preferences and try to re-initiate */ - messages.onUserValuesChanged(_userValues => { - userValues = _userValues - update() - }) + messages.onUserValuesChanged((_userValues) => { + userValues = _userValues; + update(); + }); /** * Continue to listen for updated UI settings and try to re-initiate */ - messages.onUIValuesChanged(_ui => { - ui = _ui - update() - }) + messages.onUIValuesChanged((_ui) => { + ui = _ui; + update(); + }); } /** * @param {OverlayOptions} options * @returns {Thumbnails | ClickInterception | null} */ -function thumbnailsFeatureFromOptions (options) { - return thumbnailOverlays(options) || clickInterceptions(options) +function thumbnailsFeatureFromOptions(options) { + return thumbnailOverlays(options) || clickInterceptions(options); } /** * @param {OverlayOptions} options * @return {Thumbnails | null} */ -function thumbnailOverlays ({ userValues, settings, messages, environment, ui }) { +function thumbnailOverlays({ userValues, settings, messages, environment, ui }) { // bail if not enabled remotely - if (settings.thumbnailOverlays.state !== 'enabled') return null + if (settings.thumbnailOverlays.state !== 'enabled') return null; const conditions = [ // must be in 'always ask' mode @@ -120,57 +120,57 @@ function thumbnailOverlays ({ userValues, settings, messages, environment, ui }) // must not be set to play in DuckPlayer ui?.playInDuckPlayer !== true, // must be a desktop layout - environment.layout === 'desktop' - ] + environment.layout === 'desktop', + ]; // Only show thumbnails if ALL conditions above are met - if (!conditions.every(Boolean)) return null + if (!conditions.every(Boolean)) return null; return new Thumbnails({ environment, settings, - messages - }) + messages, + }); } /** * @param {OverlayOptions} options * @return {ClickInterception | null} */ -function clickInterceptions ({ userValues, settings, messages, environment, ui }) { +function clickInterceptions({ userValues, settings, messages, environment, ui }) { // bail if not enabled remotely - if (settings.clickInterception.state !== 'enabled') return null + if (settings.clickInterception.state !== 'enabled') return null; const conditions = [ // either enabled via prefs 'enabled' in userValues.privatePlayerMode, // or has a one-time override - ui?.playInDuckPlayer === true - ] + ui?.playInDuckPlayer === true, + ]; // Intercept clicks if ANY of the conditions above are met - if (!conditions.some(Boolean)) return null + if (!conditions.some(Boolean)) return null; return new ClickInterception({ environment, settings, - messages - }) + messages, + }); } /** * @param {OverlayOptions} options * @returns {VideoOverlay | undefined} */ -function videoOverlaysFeatureFromSettings ({ userValues, settings, messages, environment, ui }) { - if (settings.videoOverlays.state !== 'enabled') return undefined +function videoOverlaysFeatureFromSettings({ userValues, settings, messages, environment, ui }) { + if (settings.videoOverlays.state !== 'enabled') return undefined; - return new VideoOverlay({ userValues, settings, environment, messages, ui }) + return new VideoOverlay({ userValues, settings, environment, messages, ui }); } export class Environment { - allowedProxyOrigins = ['duckduckgo.com'] - _strings = JSON.parse(strings) + allowedProxyOrigins = ['duckduckgo.com']; + _strings = JSON.parse(strings); /** * @param {object} params @@ -179,17 +179,17 @@ export class Environment { * @param {ImportMeta['injectName']} params.injectName * @param {string} params.locale */ - constructor (params) { - this.debug = Boolean(params.debug) - this.injectName = params.injectName - this.platform = params.platform - this.locale = params.locale + constructor(params) { + this.debug = Boolean(params.debug); + this.injectName = params.injectName; + this.platform = params.platform; + this.locale = params.locale; } - get strings () { - const matched = this._strings[this.locale] - if (matched) return matched['overlays.json'] - return this._strings.en['overlays.json'] + get strings() { + const matched = this._strings[this.locale]; + if (matched) return matched['overlays.json']; + return this._strings.en['overlays.json']; } /** @@ -197,83 +197,83 @@ export class Environment { * It's abstracted so that we can mock it in tests * @return {string} */ - getPlayerPageHref () { + getPlayerPageHref() { if (this.debug) { - const url = new URL(window.location.href) - if (url.hostname === 'www.youtube.com') return window.location.href + const url = new URL(window.location.href); + if (url.hostname === 'www.youtube.com') return window.location.href; // reflect certain query params, this is useful for testing if (url.searchParams.has('v')) { - const base = new URL('/watch', 'https://youtube.com') - base.searchParams.set('v', url.searchParams.get('v') || '') - return base.toString() + const base = new URL('/watch', 'https://youtube.com'); + base.searchParams.set('v', url.searchParams.get('v') || ''); + return base.toString(); } - return 'https://youtube.com/watch?v=123' + return 'https://youtube.com/watch?v=123'; } - return window.location.href + return window.location.href; } - getLargeThumbnailSrc (videoId) { - const url = new URL(`/vi/${videoId}/maxresdefault.jpg`, 'https://i.ytimg.com') - return url.href + getLargeThumbnailSrc(videoId) { + const url = new URL(`/vi/${videoId}/maxresdefault.jpg`, 'https://i.ytimg.com'); + return url.href; } - setHref (href) { - window.location.href = href + setHref(href) { + window.location.href = href; } - hasOneTimeOverride () { + hasOneTimeOverride() { try { // #ddg-play is a hard requirement, regardless of referrer - if (window.location.hash !== '#ddg-play') return false + if (window.location.hash !== '#ddg-play') return false; // double-check that we have something that might be a parseable URL - if (typeof document.referrer !== 'string') return false - if (document.referrer.length === 0) return false // can be empty! + if (typeof document.referrer !== 'string') return false; + if (document.referrer.length === 0) return false; // can be empty! - const { hostname } = new URL(document.referrer) - const isAllowed = this.allowedProxyOrigins.includes(hostname) - return isAllowed + const { hostname } = new URL(document.referrer); + const isAllowed = this.allowedProxyOrigins.includes(hostname); + return isAllowed; } catch (e) { - console.error(e) + console.error(e); } - return false + return false; } - isIntegrationMode () { - return this.debug === true && this.injectName === 'integration' + isIntegrationMode() { + return this.debug === true && this.injectName === 'integration'; } - isTestMode () { - return this.debug === true + isTestMode() { + return this.debug === true; } - get opensVideoOverlayLinksViaMessage () { - return this.platform.name !== 'windows' + get opensVideoOverlayLinksViaMessage() { + return this.platform.name !== 'windows'; } /** * @return {boolean} */ - get isMobile () { - return this.platform.name === 'ios' || this.platform.name === 'android' + get isMobile() { + return this.platform.name === 'ios' || this.platform.name === 'android'; } /** * @return {boolean} */ - get isDesktop () { - return !this.isMobile + get isDesktop() { + return !this.isMobile; } /** * @return {'desktop' | 'mobile'} */ - get layout () { + get layout() { if (this.platform.name === 'ios' || this.platform.name === 'android') { - return 'mobile' + return 'mobile'; } - return 'desktop' + return 'desktop'; } } diff --git a/injected/src/features/duckplayer/text.js b/injected/src/features/duckplayer/text.js index dc3d19adb..60e33f010 100644 --- a/injected/src/features/duckplayer/text.js +++ b/injected/src/features/duckplayer/text.js @@ -1,69 +1,68 @@ -import { html } from '../../dom-utils' +import { html } from '../../dom-utils'; /** * If this get's localised in the future, this would likely be in a json file */ const text = { playText: { - title: 'Duck Player' + title: 'Duck Player', }, videoOverlayTitle: { - title: 'Tired of targeted YouTube ads and recommendations?' + title: 'Tired of targeted YouTube ads and recommendations?', }, videoOverlayTitle2: { - title: 'Turn on Duck Player to watch without targeted ads' + title: 'Turn on Duck Player to watch without targeted ads', }, videoOverlayTitle3: { - title: 'Drowning in ads on YouTube? {newline} Turn on Duck Player.' + title: 'Drowning in ads on YouTube? {newline} Turn on Duck Player.', }, videoOverlaySubtitle: { - title: 'provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations.' + title: 'provides a clean viewing experience without personalized ads and prevents viewing activity from influencing your YouTube recommendations.', }, videoOverlaySubtitle2: { - title: 'What you watch in DuckDuckGo won’t influence your recommendations on YouTube.' + title: 'What you watch in DuckDuckGo won’t influence your recommendations on YouTube.', }, videoButtonOpen: { - title: 'Watch in Duck Player' + title: 'Watch in Duck Player', }, videoButtonOpen2: { - title: 'Turn On Duck Player' + title: 'Turn On Duck Player', }, videoButtonOptOut: { - title: 'Watch Here' + title: 'Watch Here', }, videoButtonOptOut2: { - title: 'No Thanks' + title: 'No Thanks', }, rememberLabel: { - title: 'Remember my choice' - } -} + title: 'Remember my choice', + }, +}; export const i18n = { /** * @param {keyof text} name */ - t (name) { + t(name) { // eslint-disable-next-line no-prototype-builtins if (!text.hasOwnProperty(name)) { - console.error(`missing key ${name}`) - return 'missing' + console.error(`missing key ${name}`); + return 'missing'; } - const match = text[name] + const match = text[name]; if (!match.title) { - return 'missing' + return 'missing'; } - return match.title - } -} + return match.title; + }, +}; /** * Converts occurrences of {newline} in a string to
tags * @param {string} text */ -export function nl2br (text) { - return html`${text.split('{newline}') - .map((line, i) => i === 0 ? line : html`
${line}`)}` +export function nl2br(text) { + return html`${text.split('{newline}').map((line, i) => (i === 0 ? line : html`
${line}`))}`; } /** @@ -88,9 +87,9 @@ export const overlayCopyVariants = { subtitle: i18n.t('videoOverlaySubtitle2'), buttonOptOut: i18n.t('videoButtonOptOut2'), buttonOpen: i18n.t('videoButtonOpen2'), - rememberLabel: i18n.t('rememberLabel') - } -} + rememberLabel: i18n.t('rememberLabel'), + }, +}; /** * @param {Record} lookup @@ -102,6 +101,6 @@ export const mobileStrings = (lookup) => { subtitle: lookup.videoOverlaySubtitle2, buttonOptOut: lookup.videoButtonOptOut2, buttonOpen: lookup.videoButtonOpen2, - rememberLabel: lookup.rememberLabel - } -} + rememberLabel: lookup.rememberLabel, + }; +}; diff --git a/injected/src/features/duckplayer/thumbnails.js b/injected/src/features/duckplayer/thumbnails.js index 08cd4de1a..aad5b64f8 100644 --- a/injected/src/features/duckplayer/thumbnails.js +++ b/injected/src/features/duckplayer/thumbnails.js @@ -52,10 +52,10 @@ * @module Duck Player Thumbnails */ -import { SideEffects, VideoParams } from './util.js' -import { IconOverlay } from './icon-overlay.js' -import { Environment } from './overlays.js' -import { OpenInDuckPlayerMsg, Pixel } from './overlay-messages.js' +import { SideEffects, VideoParams } from './util.js'; +import { IconOverlay } from './icon-overlay.js'; +import { Environment } from './overlays.js'; +import { OpenInDuckPlayerMsg, Pixel } from './overlay-messages.js'; /** * @typedef ThumbnailParams @@ -68,176 +68,176 @@ import { OpenInDuckPlayerMsg, Pixel } from './overlay-messages.js' * This features covers the implementation */ export class Thumbnails { - sideEffects = new SideEffects() + sideEffects = new SideEffects(); /** * @param {ThumbnailParams} params */ - constructor (params) { - this.settings = params.settings - this.messages = params.messages - this.environment = params.environment + constructor(params) { + this.settings = params.settings; + this.messages = params.messages; + this.environment = params.environment; } /** * Perform side effects */ - init () { + init() { this.sideEffects.add('showing overlays on hover', () => { - const { selectors } = this.settings - const parentNode = document.documentElement || document.body + const { selectors } = this.settings; + const parentNode = document.documentElement || document.body; // create the icon & append it to the page - const icon = new IconOverlay() + const icon = new IconOverlay(); icon.appendHoverOverlay((href) => { if (this.environment.opensVideoOverlayLinksViaMessage) { - this.messages.sendPixel(new Pixel({ name: 'play.use.thumbnail' })) + this.messages.sendPixel(new Pixel({ name: 'play.use.thumbnail' })); } - this.messages.openDuckPlayer(new OpenInDuckPlayerMsg({ href })) - }) + this.messages.openDuckPlayer(new OpenInDuckPlayerMsg({ href })); + }); // remember when a none-dax click occurs - so that we can avoid re-adding the // icon whilst the page is navigating - let clicked = false + let clicked = false; // detect all click, if it's anywhere on the page // but in the icon overlay itself, then just hide the overlay const clickHandler = (e) => { - const overlay = icon.getHoverOverlay() + const overlay = icon.getHoverOverlay(); if (overlay?.contains(e.target)) { // do nothing here, the click will have been handled by the overlay } else if (overlay) { - clicked = true - icon.hideOverlay(overlay) - icon.hoverOverlayVisible = false + clicked = true; + icon.hideOverlay(overlay); + icon.hoverOverlayVisible = false; setTimeout(() => { - clicked = false - }, 0) + clicked = false; + }, 0); } - } + }; - parentNode.addEventListener('click', clickHandler, true) + parentNode.addEventListener('click', clickHandler, true); const removeOverlay = () => { - const overlay = icon.getHoverOverlay() + const overlay = icon.getHoverOverlay(); if (overlay) { - icon.hideOverlay(overlay) - icon.hoverOverlayVisible = false + icon.hideOverlay(overlay); + icon.hoverOverlayVisible = false; } - } + }; const appendOverlay = (element) => { if (element && element.isConnected) { - icon.moveHoverOverlayToVideoElement(element) + icon.moveHoverOverlayToVideoElement(element); } - } + }; // detect hovers and decide to show hover icon, or not const mouseOverHandler = (e) => { - if (clicked) return - const hoverElement = findElementFromEvent(selectors.thumbLink, selectors.hoverExcluded, e) - const validLink = isValidLink(hoverElement, selectors.excludedRegions) + if (clicked) return; + const hoverElement = findElementFromEvent(selectors.thumbLink, selectors.hoverExcluded, e); + const validLink = isValidLink(hoverElement, selectors.excludedRegions); // if it's not an element we care about, bail early and remove the overlay if (!hoverElement || !validLink) { - return removeOverlay() + return removeOverlay(); } // ensure it doesn't contain sub-links if (hoverElement.querySelector('a[href]')) { - return removeOverlay() + return removeOverlay(); } // only add Dax when this link also contained an img if (!hoverElement.querySelector('img')) { - return removeOverlay() + return removeOverlay(); } // if the hover target is the match, or contains the match, all good if (e.target === hoverElement || hoverElement?.contains(e.target)) { - return appendOverlay(hoverElement) + return appendOverlay(hoverElement); } // finally, check the 'allowedEventTargets' to see if the hover occurred in an element // that we know to be a thumbnail overlay, like a preview - const matched = selectors.allowedEventTargets.find(css => e.target.matches(css)) + const matched = selectors.allowedEventTargets.find((css) => e.target.matches(css)); if (matched) { - appendOverlay(hoverElement) + appendOverlay(hoverElement); } - } + }; - parentNode.addEventListener('mouseover', mouseOverHandler, true) + parentNode.addEventListener('mouseover', mouseOverHandler, true); return () => { - parentNode.removeEventListener('mouseover', mouseOverHandler, true) - parentNode.removeEventListener('click', clickHandler, true) - icon.destroy() - } - }) + parentNode.removeEventListener('mouseover', mouseOverHandler, true); + parentNode.removeEventListener('click', clickHandler, true); + icon.destroy(); + }; + }); } - destroy () { - this.sideEffects.destroy() + destroy() { + this.sideEffects.destroy(); } } export class ClickInterception { - sideEffects = new SideEffects() + sideEffects = new SideEffects(); /** * @param {ThumbnailParams} params */ - constructor (params) { - this.settings = params.settings - this.messages = params.messages - this.environment = params.environment + constructor(params) { + this.settings = params.settings; + this.messages = params.messages; + this.environment = params.environment; } /** * Perform side effects */ - init () { + init() { this.sideEffects.add('intercepting clicks', () => { - const { selectors } = this.settings - const parentNode = document.documentElement || document.body + const { selectors } = this.settings; + const parentNode = document.documentElement || document.body; const clickHandler = (e) => { - const elementInStack = findElementFromEvent(selectors.thumbLink, selectors.clickExcluded, e) - const validLink = isValidLink(elementInStack, selectors.excludedRegions) + const elementInStack = findElementFromEvent(selectors.thumbLink, selectors.clickExcluded, e); + const validLink = isValidLink(elementInStack, selectors.excludedRegions); const block = (href) => { - e.preventDefault() - e.stopImmediatePropagation() - this.messages.openDuckPlayer({ href }) - } + e.preventDefault(); + e.stopImmediatePropagation(); + this.messages.openDuckPlayer({ href }); + }; // if there's no match, return early if (!validLink) { - return + return; } // if the hover target is the match, or contains the match, all good if (e.target === elementInStack || elementInStack?.contains(e.target)) { - return block(validLink) + return block(validLink); } // finally, check the 'allowedEventTargets' to see if the hover occurred in an element // that we know to be a thumbnail overlay, like a preview - const matched = selectors.allowedEventTargets.find(css => e.target.matches(css)) + const matched = selectors.allowedEventTargets.find((css) => e.target.matches(css)); if (matched) { - block(validLink) + block(validLink); } - } + }; - parentNode.addEventListener('click', clickHandler, true) + parentNode.addEventListener('click', clickHandler, true); return () => { - parentNode.removeEventListener('click', clickHandler, true) - } - }) + parentNode.removeEventListener('click', clickHandler, true); + }; + }); } - destroy () { - this.sideEffects.destroy() + destroy() { + this.sideEffects.destroy(); } } @@ -247,16 +247,16 @@ export class ClickInterception { * @param {MouseEvent} e * @return {HTMLElement|null} */ -function findElementFromEvent (selector, excludedSelectors, e) { +function findElementFromEvent(selector, excludedSelectors, e) { /** @type {HTMLElement | null} */ - let matched = null + let matched = null; - const fastPath = excludedSelectors.length === 0 + const fastPath = excludedSelectors.length === 0; for (const element of document.elementsFromPoint(e.clientX, e.clientY)) { // bail early if this item was excluded anywhere in the element stack - if (excludedSelectors.some(ex => element.matches(ex))) { - return null + if (excludedSelectors.some((ex) => element.matches(ex))) { + return null; } // we cannot return this immediately, because another element in the stack @@ -264,11 +264,11 @@ function findElementFromEvent (selector, excludedSelectors, e) { if (element.matches(selector)) { // in lots of cases we can just return the element as soon as it's found, to prevent // checking the entire stack - matched = /** @type {HTMLElement} */(element) - if (fastPath) return matched + matched = /** @type {HTMLElement} */ (element); + if (fastPath) return matched; } } - return matched + return matched; } /** @@ -276,36 +276,36 @@ function findElementFromEvent (selector, excludedSelectors, e) { * @param {string[]} excludedRegions * @return {string | null | undefined} */ -function isValidLink (element, excludedRegions) { - if (!element) return null +function isValidLink(element, excludedRegions) { + if (!element) return null; /** * Does this element exist inside an excluded region? */ - const existsInExcludedParent = excludedRegions.some(selector => { + const existsInExcludedParent = excludedRegions.some((selector) => { for (const parent of document.querySelectorAll(selector)) { - if (parent.contains(element)) return true + if (parent.contains(element)) return true; } - return false - }) + return false; + }); /** * Does this element exist inside an excluded region? * If so, bail */ - if (existsInExcludedParent) return null + if (existsInExcludedParent) return null; /** * We shouldn't be able to get here, but this keeps Typescript happy * and is a good check regardless */ - if (!('href' in element)) return null + if (!('href' in element)) return null; /** * If we get here, we're trying to convert the `element.href` * into a valid Duck Player URL */ - return VideoParams.fromHref(element.href)?.toPrivatePlayerUrl() + return VideoParams.fromHref(element.href)?.toPrivatePlayerUrl(); } -export { SideEffects, VideoParams, Environment } +export { SideEffects, VideoParams, Environment }; diff --git a/injected/src/features/duckplayer/util.js b/injected/src/features/duckplayer/util.js index 0bef42dcb..df3f2a1a7 100644 --- a/injected/src/features/duckplayer/util.js +++ b/injected/src/features/duckplayer/util.js @@ -5,12 +5,12 @@ * @param {string} event * @param {function} callback */ -export function addTrustedEventListener (element, event, callback) { +export function addTrustedEventListener(element, event, callback) { element.addEventListener(event, (e) => { if (e.isTrusted) { - callback(e) + callback(e); } - }) + }); } /** @@ -20,8 +20,8 @@ export function addTrustedEventListener (element, event, callback) { * @param {string} targetSelector * @param {string} imageUrl */ -export function appendImageAsBackground (parent, targetSelector, imageUrl) { - const canceled = false +export function appendImageAsBackground(parent, targetSelector, imageUrl) { + const canceled = false; /** * Make a HEAD request to see what the status of this image is, without @@ -30,52 +30,55 @@ export function appendImageAsBackground (parent, targetSelector, imageUrl) { * This is needed because YouTube returns a 404 + valid image file when there's no * thumbnail and you can't tell the difference through the 'onload' event alone */ - fetch(imageUrl, { method: 'HEAD' }).then(x => { - const status = String(x.status) - if (canceled) return console.warn('not adding image, cancelled') - if (status.startsWith('2')) { - if (!canceled) { - append() + fetch(imageUrl, { method: 'HEAD' }) + .then((x) => { + const status = String(x.status); + if (canceled) return console.warn('not adding image, cancelled'); + if (status.startsWith('2')) { + if (!canceled) { + append(); + } else { + console.warn('ignoring cancelled load'); + } } else { - console.warn('ignoring cancelled load') + markError(); } - } else { - markError() - } - }).catch(() => { - console.error('e from fetch') - }) + }) + .catch(() => { + console.error('e from fetch'); + }); /** * If loading fails, mark the parent with data-attributes */ - function markError () { - parent.dataset.thumbLoaded = String(false) - parent.dataset.error = String(true) + function markError() { + parent.dataset.thumbLoaded = String(false); + parent.dataset.error = String(true); } /** * If loading succeeds, try to append the image */ - function append () { - const targetElement = parent.querySelector(targetSelector) - if (!(targetElement instanceof HTMLElement)) return console.warn('could not find child with selector', targetSelector, 'from', parent) - parent.dataset.thumbLoaded = String(true) - parent.dataset.thumbSrc = imageUrl - const img = new Image() - img.src = imageUrl + function append() { + const targetElement = parent.querySelector(targetSelector); + if (!(targetElement instanceof HTMLElement)) + return console.warn('could not find child with selector', targetSelector, 'from', parent); + parent.dataset.thumbLoaded = String(true); + parent.dataset.thumbSrc = imageUrl; + const img = new Image(); + img.src = imageUrl; img.onload = function () { - if (canceled) return console.warn('not adding image, cancelled') - targetElement.style.backgroundImage = `url(${imageUrl})` - targetElement.style.backgroundSize = 'cover' - } + if (canceled) return console.warn('not adding image, cancelled'); + targetElement.style.backgroundImage = `url(${imageUrl})`; + targetElement.style.backgroundSize = 'cover'; + }; img.onerror = function () { - if (canceled) return console.warn('not calling markError, cancelled') - markError() - const targetElement = parent.querySelector(targetSelector) - if (!(targetElement instanceof HTMLElement)) return - targetElement.style.backgroundImage = '' - } + if (canceled) return console.warn('not calling markError, cancelled'); + markError(); + const targetElement = parent.querySelector(targetSelector); + if (!(targetElement instanceof HTMLElement)) return; + targetElement.style.backgroundImage = ''; + }; } } @@ -84,51 +87,51 @@ export class SideEffects { * @param {object} params * @param {boolean} [params.debug] */ - constructor ({ debug = false } = { }) { - this.debug = debug + constructor({ debug = false } = {}) { + this.debug = debug; } /** @type {{fn: () => void, name: string}[]} */ - _cleanups = [] + _cleanups = []; /** * Wrap a side-effecting operation for easier debugging * and teardown/release of resources * @param {string} name * @param {() => () => void} fn */ - add (name, fn) { + add(name, fn) { try { if (this.debug) { - console.log('☢️', name) + console.log('☢️', name); } - const cleanup = fn() + const cleanup = fn(); if (typeof cleanup === 'function') { - this._cleanups.push({ name, fn: cleanup }) + this._cleanups.push({ name, fn: cleanup }); } } catch (e) { - console.error('%s threw an error', name, e) + console.error('%s threw an error', name, e); } } /** * Remove elements, event listeners etc */ - destroy () { + destroy() { for (const cleanup of this._cleanups) { if (typeof cleanup.fn === 'function') { try { if (this.debug) { - console.log('🗑️', cleanup.name) + console.log('🗑️', cleanup.name); } - cleanup.fn() + cleanup.fn(); } catch (e) { - console.error(`cleanup ${cleanup.name} threw`, e) + console.error(`cleanup ${cleanup.name} threw`, e); } } else { - throw new Error('invalid cleanup') + throw new Error('invalid cleanup'); } } - this._cleanups = [] + this._cleanups = []; } } @@ -152,27 +155,27 @@ export class VideoParams { * @param {string} id - the YouTube video ID * @param {string|null|undefined} time - an optional time */ - constructor (id, time) { - this.id = id - this.time = time + constructor(id, time) { + this.id = id; + this.time = time; } - static validVideoId = /^[a-zA-Z0-9-_]+$/ - static validTimestamp = /^[0-9hms]+$/ + static validVideoId = /^[a-zA-Z0-9-_]+$/; + static validTimestamp = /^[0-9hms]+$/; /** * @returns {string} */ - toPrivatePlayerUrl () { + toPrivatePlayerUrl() { // no try/catch because we already validated the ID // in Microsoft WebView2 v118+ changing from special protocol (https) to non-special one (duck) is forbidden // so we need to construct duck player this way - const duckUrl = new URL(`duck://player/${this.id}`) + const duckUrl = new URL(`duck://player/${this.id}`); if (this.time) { - duckUrl.searchParams.set('t', this.time) + duckUrl.searchParams.set('t', this.time); } - return duckUrl.href + return duckUrl.href; } /** @@ -181,17 +184,17 @@ export class VideoParams { * @param {string} href * @returns {VideoParams|null} */ - static forWatchPage (href) { - let url + static forWatchPage(href) { + let url; try { - url = new URL(href) + url = new URL(href); } catch (e) { - return null + return null; } if (!url.pathname.startsWith('/watch')) { - return null + return null; } - return VideoParams.fromHref(url.href) + return VideoParams.fromHref(url.href); } /** @@ -200,14 +203,14 @@ export class VideoParams { * @param pathname * @returns {VideoParams|null} */ - static fromPathname (pathname) { - let url + static fromPathname(pathname) { + let url; try { - url = new URL(pathname, window.location.origin) + url = new URL(pathname, window.location.origin); } catch (e) { - return null + return null; } - return VideoParams.fromHref(url.href) + return VideoParams.fromHref(url.href); } /** @@ -217,43 +220,43 @@ export class VideoParams { * @param href * @returns {VideoParams|null} */ - static fromHref (href) { - let url + static fromHref(href) { + let url; try { - url = new URL(href) + url = new URL(href); } catch (e) { - return null + return null; } - let id = null + let id = null; // known params - const vParam = url.searchParams.get('v') - const tParam = url.searchParams.get('t') + const vParam = url.searchParams.get('v'); + const tParam = url.searchParams.get('t'); // don't continue if 'list' is present, but 'index' is not. // valid: '/watch?v=321&list=123&index=1234' // invalid: '/watch?v=321&list=123' <- index absent if (url.searchParams.has('list') && !url.searchParams.has('index')) { - return null + return null; } - let time = null + let time = null; // ensure youtube video id is good if (vParam && VideoParams.validVideoId.test(vParam)) { - id = vParam + id = vParam; } else { // if the video ID is invalid, we cannot produce an instance of VideoParams - return null + return null; } // ensure timestamp is good, if set if (tParam && VideoParams.validTimestamp.test(tParam)) { - time = tParam + time = tParam; } - return new VideoParams(id, time) + return new VideoParams(id, time); } } @@ -264,17 +267,17 @@ export class VideoParams { * if the DOM is already loaded. */ export class DomState { - loaded = false - loadedCallbacks = [] - constructor () { + loaded = false; + loadedCallbacks = []; + constructor() { window.addEventListener('DOMContentLoaded', () => { - this.loaded = true - this.loadedCallbacks.forEach(cb => cb()) - }) + this.loaded = true; + this.loadedCallbacks.forEach((cb) => cb()); + }); } - onLoaded (loadedCallback) { - if (this.loaded) return loadedCallback() - this.loadedCallbacks.push(loadedCallback) + onLoaded(loadedCallback) { + if (this.loaded) return loadedCallback(); + this.loadedCallbacks.push(loadedCallback); } } diff --git a/injected/src/features/duckplayer/video-overlay.js b/injected/src/features/duckplayer/video-overlay.js index 87564eb70..fc391f2c8 100644 --- a/injected/src/features/duckplayer/video-overlay.js +++ b/injected/src/features/duckplayer/video-overlay.js @@ -25,25 +25,25 @@ * - if the user previously clicked 'watch here + remember', just add the small dax * - otherwise, stop the video playing + append our overlay */ -import { SideEffects, VideoParams } from './util.js' -import { DDGVideoOverlay } from './components/ddg-video-overlay.js' -import { OpenInDuckPlayerMsg, Pixel } from './overlay-messages.js' -import { IconOverlay } from './icon-overlay.js' -import { mobileStrings } from './text.js' -import { DDGVideoOverlayMobile } from './components/ddg-video-overlay-mobile.js' +import { SideEffects, VideoParams } from './util.js'; +import { DDGVideoOverlay } from './components/ddg-video-overlay.js'; +import { OpenInDuckPlayerMsg, Pixel } from './overlay-messages.js'; +import { IconOverlay } from './icon-overlay.js'; +import { mobileStrings } from './text.js'; +import { DDGVideoOverlayMobile } from './components/ddg-video-overlay-mobile.js'; /** * Handle the switch between small & large overlays * + conduct any communications */ export class VideoOverlay { - sideEffects = new SideEffects() + sideEffects = new SideEffects(); /** @type {string | null} */ - lastVideoId = null + lastVideoId = null; /** @type {boolean} */ - didAllowFirstVideo = false + didAllowFirstVideo = false; /** * @param {object} options @@ -53,37 +53,37 @@ export class VideoOverlay { * @param {import("./overlay-messages.js").DuckPlayerOverlayMessages} options.messages * @param {import("../duck-player.js").UISettings} options.ui */ - constructor ({ userValues, settings, environment, messages, ui }) { - this.userValues = userValues - this.settings = settings - this.environment = environment - this.messages = messages - this.ui = ui + constructor({ userValues, settings, environment, messages, ui }) { + this.userValues = userValues; + this.settings = settings; + this.environment = environment; + this.messages = messages; + this.ui = ui; } /** * @param {'page-load' | 'preferences-changed' | 'href-changed'} trigger */ - init (trigger) { + init(trigger) { if (trigger === 'page-load') { - this.handleFirstPageLoad() + this.handleFirstPageLoad(); } else if (trigger === 'preferences-changed') { - this.watchForVideoBeingAdded({ via: 'user notification', ignoreCache: true }) + this.watchForVideoBeingAdded({ via: 'user notification', ignoreCache: true }); } else if (trigger === 'href-changed') { - this.watchForVideoBeingAdded({ via: 'href changed' }) + this.watchForVideoBeingAdded({ via: 'href changed' }); } } /** * Special handling of a first-page, an attempt to load our overlay as quickly as possible */ - handleFirstPageLoad () { + handleFirstPageLoad() { // don't continue unless we're in 'alwaysAsk' mode - if ('disabled' in this.userValues.privatePlayerMode) return + if ('disabled' in this.userValues.privatePlayerMode) return; // don't continue if we can't derive valid video params - const validParams = VideoParams.forWatchPage(this.environment.getPlayerPageHref()) - if (!validParams) return + const validParams = VideoParams.forWatchPage(this.environment.getPlayerPageHref()); + if (!validParams) return; /** * If we get here, we know the following: @@ -96,60 +96,60 @@ export class VideoOverlay { * Later, when our overlay loads that CSS will be removed in the cleanup. */ this.sideEffects.add('add css to head', () => { - const style = document.createElement('style') - style.innerText = this.settings.selectors.videoElementContainer + ' { opacity: 0!important }' + const style = document.createElement('style'); + style.innerText = this.settings.selectors.videoElementContainer + ' { opacity: 0!important }'; if (document.head) { - document.head.appendChild(style) + document.head.appendChild(style); } return () => { if (style.isConnected) { - document.head.removeChild(style) + document.head.removeChild(style); } - } - }) + }; + }); /** * Keep trying to find the video element every 100 ms */ this.sideEffects.add('wait for first video element', () => { const int = setInterval(() => { - this.watchForVideoBeingAdded({ via: 'first page load' }) - }, 100) + this.watchForVideoBeingAdded({ via: 'first page load' }); + }, 100); return () => { - clearInterval(int) - } - }) + clearInterval(int); + }; + }); } /** * @param {import("./util").VideoParams} params */ - addSmallDaxOverlay (params) { - const containerElement = document.querySelector(this.settings.selectors.videoElementContainer) + addSmallDaxOverlay(params) { + const containerElement = document.querySelector(this.settings.selectors.videoElementContainer); if (!containerElement || !(containerElement instanceof HTMLElement)) { - console.error('no container element') - return + console.error('no container element'); + return; } this.sideEffects.add('adding small dax 🐥 icon overlay', () => { - const href = params.toPrivatePlayerUrl() + const href = params.toPrivatePlayerUrl(); - const icon = new IconOverlay() + const icon = new IconOverlay(); icon.appendSmallVideoOverlay(containerElement, href, (href) => { - this.messages.openDuckPlayer(new OpenInDuckPlayerMsg({ href })) - }) + this.messages.openDuckPlayer(new OpenInDuckPlayerMsg({ href })); + }); return () => { - icon.destroy() - } - }) + icon.destroy(); + }; + }); } /** * @param {{ignoreCache?: boolean, via?: string}} [opts] */ - watchForVideoBeingAdded (opts = {}) { - const params = VideoParams.forWatchPage(this.environment.getPlayerPageHref()) + watchForVideoBeingAdded(opts = {}) { + const params = VideoParams.forWatchPage(this.environment.getPlayerPageHref()); if (!params) { /** @@ -157,10 +157,10 @@ export class VideoOverlay { * it's likely a 'back' navigation by the user, so we should always try to remove all overlays */ if (this.lastVideoId) { - this.destroy() - this.lastVideoId = null + this.destroy(); + this.lastVideoId = null; } - return + return; } const conditions = [ @@ -169,55 +169,55 @@ export class VideoOverlay { // first visit !this.lastVideoId, // new video id - this.lastVideoId && this.lastVideoId !== params.id // different - ] + this.lastVideoId && this.lastVideoId !== params.id, // different + ]; if (conditions.some(Boolean)) { /** * Don't continue until we've been able to find the HTML elements that we inject into */ - const videoElement = document.querySelector(this.settings.selectors.videoElement) - const playerContainer = document.querySelector(this.settings.selectors.videoElementContainer) + const videoElement = document.querySelector(this.settings.selectors.videoElement); + const playerContainer = document.querySelector(this.settings.selectors.videoElementContainer); if (!videoElement || !playerContainer) { - return null + return null; } /** * If we get here, it's a valid situation */ - const userValues = this.userValues - this.lastVideoId = params.id + const userValues = this.userValues; + this.lastVideoId = params.id; /** * always remove everything first, to prevent any lingering state */ - this.destroy() + this.destroy(); /** * When enabled, just show the small dax icon */ if ('enabled' in userValues.privatePlayerMode) { - return this.addSmallDaxOverlay(params) + return this.addSmallDaxOverlay(params); } if ('alwaysAsk' in userValues.privatePlayerMode) { // if there's a one-time-override (eg: a link from the serp), then do nothing - if (this.environment.hasOneTimeOverride()) return + if (this.environment.hasOneTimeOverride()) return; // should the first video be allowed to play? if (this.ui.allowFirstVideo === true && !this.didAllowFirstVideo) { - this.didAllowFirstVideo = true - return console.count('Allowing the first video') + this.didAllowFirstVideo = true; + return console.count('Allowing the first video'); } // if the user previously clicked 'watch here + remember', just add the small dax if (this.userValues.overlayInteracted) { - return this.addSmallDaxOverlay(params) + return this.addSmallDaxOverlay(params); } // if we get here, we're trying to prevent the video playing - this.stopVideoFromPlaying() - this.appendOverlayToPage(playerContainer, params) + this.stopVideoFromPlaying(); + this.appendOverlayToPage(playerContainer, params); } } } @@ -226,76 +226,74 @@ export class VideoOverlay { * @param {Element} targetElement * @param {import("./util").VideoParams} params */ - appendOverlayToPage (targetElement, params) { + appendOverlayToPage(targetElement, params) { this.sideEffects.add(`appending ${DDGVideoOverlay.CUSTOM_TAG_NAME} or ${DDGVideoOverlayMobile.CUSTOM_TAG_NAME} to the page`, () => { - this.messages.sendPixel(new Pixel({ name: 'overlay' })) - const controller = new AbortController() - const { environment } = this + this.messages.sendPixel(new Pixel({ name: 'overlay' })); + const controller = new AbortController(); + const { environment } = this; if (this.environment.layout === 'mobile') { - const elem = /** @type {DDGVideoOverlayMobile} */(document.createElement(DDGVideoOverlayMobile.CUSTOM_TAG_NAME)) - elem.testMode = this.environment.isTestMode() - elem.text = mobileStrings(this.environment.strings) - elem.addEventListener(DDGVideoOverlayMobile.OPEN_INFO, () => this.messages.openInfo()) - elem.addEventListener(DDGVideoOverlayMobile.OPT_OUT, (/** @type {CustomEvent<{remember: boolean}>} */e) => { - return this.mobileOptOut(e.detail.remember) - .catch(console.error) - }) - elem.addEventListener(DDGVideoOverlayMobile.OPT_IN, (/** @type {CustomEvent<{remember: boolean}>} */e) => { - return this.mobileOptIn(e.detail.remember, params) - .catch(console.error) - }) - targetElement.appendChild(elem) + const elem = /** @type {DDGVideoOverlayMobile} */ (document.createElement(DDGVideoOverlayMobile.CUSTOM_TAG_NAME)); + elem.testMode = this.environment.isTestMode(); + elem.text = mobileStrings(this.environment.strings); + elem.addEventListener(DDGVideoOverlayMobile.OPEN_INFO, () => this.messages.openInfo()); + elem.addEventListener(DDGVideoOverlayMobile.OPT_OUT, (/** @type {CustomEvent<{remember: boolean}>} */ e) => { + return this.mobileOptOut(e.detail.remember).catch(console.error); + }); + elem.addEventListener(DDGVideoOverlayMobile.OPT_IN, (/** @type {CustomEvent<{remember: boolean}>} */ e) => { + return this.mobileOptIn(e.detail.remember, params).catch(console.error); + }); + targetElement.appendChild(elem); } else { const elem = new DDGVideoOverlay({ environment, params, ui: this.ui, - manager: this - }) - targetElement.appendChild(elem) + manager: this, + }); + targetElement.appendChild(elem); } /** * To cleanup just find and remove the element */ return () => { - document.querySelector(DDGVideoOverlay.CUSTOM_TAG_NAME)?.remove() - document.querySelector(DDGVideoOverlayMobile.CUSTOM_TAG_NAME)?.remove() - controller.abort() - } - }) + document.querySelector(DDGVideoOverlay.CUSTOM_TAG_NAME)?.remove(); + document.querySelector(DDGVideoOverlayMobile.CUSTOM_TAG_NAME)?.remove(); + controller.abort(); + }; + }); } /** * Just brute-force calling video.pause() for as long as the user is seeing the overlay. */ - stopVideoFromPlaying () { + stopVideoFromPlaying() { this.sideEffects.add(`pausing the