├── .all-contributorsrc
├── .babelrc
├── .eslintignore
├── .eslintrc.js
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── 1.bug.md
│ ├── 2.feature.md
│ └── config.yml
├── PULL_REQUEST_TEMPLATE.md
├── scripts
│ ├── env.sh
│ └── release-notes.sh
└── workflows
│ ├── cd.yml
│ └── ci.yml
├── .gitignore
├── .lintstagedrc.js
├── .node-version
├── .prettierignore
├── .prettierrc.js
├── .proxyrc
├── .well-known
└── dnt-policy.txt
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── MAINTAINING.md
├── README.md
├── ROADMAP.md
├── _redirects
├── cypress.json
├── cypress
├── e2e
│ ├── header.spec.js
│ └── initApp.spec.js
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ └── index.js
├── devtools
└── src
│ ├── background
│ └── background.js
│ ├── content-script
│ ├── content-script.js
│ ├── highlighter
│ │ ├── Highlighter.js
│ │ ├── Overlay.js
│ │ ├── index.js
│ │ └── utils.js
│ └── lib
│ │ ├── inject.js
│ │ └── onDocReady.js
│ ├── devtools
│ ├── components
│ │ ├── Icons
│ │ │ ├── InspectIcon.js
│ │ │ ├── LayersIcon.js
│ │ │ ├── LogIcon.js
│ │ │ ├── SelectIcon.js
│ │ │ └── SettingsIcon.js
│ │ └── MenuBar.js
│ ├── lib
│ │ ├── inspectedWindow.js
│ │ ├── settings.js
│ │ └── utils.js
│ ├── main.html
│ ├── main.js
│ ├── pane.html
│ ├── pane.js
│ ├── panel.html
│ └── panel.js
│ ├── manifest.json
│ └── window
│ └── testing-library.js
├── docs
├── features.md
├── icon.png
└── testing-playground-com.gif
├── logo.svg
├── netlify.toml
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── browserconfig.xml
├── code_thinking.png
├── favicon.ico
├── icon.png
├── icons
│ ├── 114-production.png
│ ├── 120-production.png
│ ├── 128-production.png
│ ├── 144-production.png
│ ├── 150-production.png
│ ├── 152-production.png
│ ├── 16-production.png
│ ├── 180-production.png
│ ├── 192-production.png
│ ├── 310-production.png
│ ├── 32-production.png
│ ├── 36-production.png
│ ├── 48-production.png
│ ├── 57-production.png
│ ├── 60-production.png
│ ├── 70-production.png
│ ├── 72-production.png
│ ├── 76-production.png
│ └── 96-production.png
├── manifest.json
└── site.jpg
├── scripts
├── build-extension.mts
├── build-lambda.mts
├── changelog.js
├── crxmake.sh
├── utils.mts
└── vite.config.mts
├── src
├── App.js
├── components
│ ├── CopyButton.js
│ ├── CopyButton.test.js
│ ├── DomEvents.js
│ ├── Editor.js
│ ├── Embed.js
│ ├── EmptyPane.js
│ ├── ErrorBox.js
│ ├── Expandable.js
│ ├── Header.js
│ ├── IconButton.js
│ ├── Input.js
│ ├── Layout.js
│ ├── Loader.js
│ ├── MarkupEditor.js
│ ├── Menu.js
│ ├── Modal.js
│ ├── PlaygroundPanels.js
│ ├── Preview.js
│ ├── PreviewHint.js
│ ├── Query.js
│ ├── QueryEditor.js
│ ├── QueryOutput.js
│ ├── Quote.js
│ ├── Result.js
│ ├── Result.test.js
│ ├── ResultQueries.js
│ ├── ResultSuggestion.js
│ ├── Scrollable.js
│ ├── Settings.js
│ ├── Share.js
│ ├── Spinner.js
│ ├── StickyList.js
│ └── TabButton.js
├── constants.js
├── context
│ └── PreviewEvents.js
├── embed.html
├── embed.js
├── gh-api
│ └── gist.js
├── hooks
│ ├── useParentMessaging.js
│ ├── usePlayground.js
│ ├── useSorter.js
│ └── useSorter.test.js
├── images
│ └── EmptyStreetImg.js
├── index.html
├── index.js
├── lambda
│ ├── gist
│ │ └── gist.js
│ ├── oembed
│ │ └── oembed.js
│ └── server
│ │ └── server.js
├── lib
│ ├── beautify.js
│ ├── cssPath.js
│ ├── deepEqual.js
│ ├── domEvents.js
│ ├── ensureArray.js
│ ├── getFieldName.js
│ ├── index.js
│ ├── logger.js
│ ├── postMessage.js
│ ├── queryAdvise.js
│ ├── queryAdvise.test.js
│ └── state
│ │ └── url.js
├── pages
│ ├── Embedded.js
│ ├── Playground.js
│ └── Playground.test.js
├── parser.js
├── sandbox.html
├── sandbox.js
├── service-worker.js
└── styles
│ ├── app.css
│ ├── codemirror.css
│ ├── index.css
│ ├── spinner.css
│ └── toggle.css
├── tailwind.config.js
├── tests
└── setupTests.js
├── tsconfig.json
└── vite.config.ts
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "testing-playground",
3 | "projectOwner": "testing-library",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": false,
11 | "commitConvention": "angular",
12 | "contributors": [
13 | {
14 | "login": "smeijer",
15 | "name": "Stephan Meijer",
16 | "avatar_url": "https://avatars1.githubusercontent.com/u/1196524?v=4",
17 | "profile": "https://github.com/smeijer",
18 | "contributions": [
19 | "ideas",
20 | "code",
21 | "infra",
22 | "maintenance"
23 | ]
24 | },
25 | {
26 | "login": "marcosvega91",
27 | "name": "Marco Moretti",
28 | "avatar_url": "https://avatars2.githubusercontent.com/u/5365582?v=4",
29 | "profile": "https://github.com/marcosvega91",
30 | "contributions": [
31 | "code",
32 | "test",
33 | "doc"
34 | ]
35 | },
36 | {
37 | "login": "timdeschryver",
38 | "name": "Tim Deschryver",
39 | "avatar_url": "https://avatars1.githubusercontent.com/u/28659384?v=4",
40 | "profile": "http://timdeschryver.dev",
41 | "contributions": [
42 | "code"
43 | ]
44 | },
45 | {
46 | "login": "kentcdodds",
47 | "name": "Kent C. Dodds",
48 | "avatar_url": "https://avatars0.githubusercontent.com/u/1500684?v=4",
49 | "profile": "https://kentcdodds.com",
50 | "contributions": [
51 | "ideas"
52 | ]
53 | },
54 | {
55 | "login": "MichaelDeBoey",
56 | "name": "Michaël De Boey",
57 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4",
58 | "profile": "https://michaeldeboey.be",
59 | "contributions": [
60 | "code"
61 | ]
62 | },
63 | {
64 | "login": "delca85",
65 | "name": "Bianca Del Carretto",
66 | "avatar_url": "https://avatars1.githubusercontent.com/u/4076043?v=4",
67 | "profile": "https://github.com/delca85",
68 | "contributions": [
69 | "code",
70 | "doc"
71 | ]
72 | },
73 | {
74 | "login": "ljosberinn",
75 | "name": "Gerrit Alex",
76 | "avatar_url": "https://avatars1.githubusercontent.com/u/29307652?v=4",
77 | "profile": "http://gerritalex.de",
78 | "contributions": [
79 | "code",
80 | "test"
81 | ]
82 | },
83 | {
84 | "login": "Siemko",
85 | "name": "Dominik Guzy",
86 | "avatar_url": "https://avatars1.githubusercontent.com/u/9118764?v=4",
87 | "profile": "https://www.guzy.dev",
88 | "contributions": [
89 | "code"
90 | ]
91 | },
92 | {
93 | "login": "connorProgrammes",
94 | "name": "ConnorProgrammes",
95 | "avatar_url": "https://avatars3.githubusercontent.com/u/66570218?v=4",
96 | "profile": "https://github.com/connorProgrammes",
97 | "contributions": [
98 | "doc",
99 | "code"
100 | ]
101 | },
102 | {
103 | "login": "JacobMGEvans",
104 | "name": "Jacob M-G Evans",
105 | "avatar_url": "https://avatars1.githubusercontent.com/u/27247160?v=4",
106 | "profile": "https://twitter.com/JacobMGEvans",
107 | "contributions": [
108 | "code",
109 | "test"
110 | ]
111 | },
112 | {
113 | "login": "sumeesh879",
114 | "name": "Sumeesh Nagisetty",
115 | "avatar_url": "https://avatars1.githubusercontent.com/u/20029120?v=4",
116 | "profile": "https://github.com/sumeesh879",
117 | "contributions": [
118 | "review"
119 | ]
120 | },
121 | {
122 | "login": "flaviohenriquecbc",
123 | "name": "Flávio H Freitas",
124 | "avatar_url": "https://avatars0.githubusercontent.com/u/1553609?v=4",
125 | "profile": "http://www.linkedin.com/in/flaviohfreitas",
126 | "contributions": [
127 | "code"
128 | ]
129 | },
130 | {
131 | "login": "bmeverett",
132 | "name": "Brandon Everett",
133 | "avatar_url": "https://avatars2.githubusercontent.com/u/3941006?v=4",
134 | "profile": "https://github.com/bmeverett",
135 | "contributions": [
136 | "code"
137 | ]
138 | },
139 | {
140 | "login": "michal-kocarek",
141 | "name": "Michal Kočárek",
142 | "avatar_url": "https://avatars1.githubusercontent.com/u/762095?v=4",
143 | "profile": "http://brainbox.cz/",
144 | "contributions": [
145 | "code",
146 | "ideas"
147 | ]
148 | },
149 | {
150 | "login": "aganglada",
151 | "name": "Alejandro Garcia Anglada",
152 | "avatar_url": "https://avatars0.githubusercontent.com/u/922348?v=4",
153 | "profile": "https://aganglada.com",
154 | "contributions": [
155 | "code",
156 | "doc",
157 | "test"
158 | ]
159 | },
160 | {
161 | "login": "ddehart",
162 | "name": "ddehart",
163 | "avatar_url": "https://avatars3.githubusercontent.com/u/901215?v=4",
164 | "profile": "https://github.com/ddehart",
165 | "contributions": [
166 | "code",
167 | "test"
168 | ]
169 | },
170 | {
171 | "login": "balavishnuvj",
172 | "name": "Balavishnu V J",
173 | "avatar_url": "https://avatars3.githubusercontent.com/u/13718688?v=4",
174 | "profile": "https://balavishnuvj.com/?utm_source=github",
175 | "contributions": [
176 | "code"
177 | ]
178 | }
179 | ],
180 | "contributorsPerLine": 7,
181 | "skipCi": true
182 | }
183 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-react", "@babel/preset-env"],
3 | "plugins": [
4 | [
5 | "@babel/plugin-proposal-class-properties",
6 | {
7 | "loose": true
8 | }
9 | ]
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .cache
2 | dist
3 | cypress
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ignorePatterns: ['!**/*'],
3 | parser: '@typescript-eslint/parser',
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:prettier/recommended',
7 | 'plugin:react/recommended',
8 | 'plugin:react-hooks/recommended',
9 | 'plugin:cypress/recommended',
10 | ],
11 | plugins: ['vitest-globals'],
12 | parserOptions: {
13 | ecmaVersion: 2020,
14 | sourceType: 'module',
15 | },
16 | settings: {
17 | react: {
18 | version: 'detect',
19 | },
20 | },
21 | env: {
22 | browser: true,
23 | node: true,
24 | es6: true,
25 | 'vitest-globals/env': true,
26 | },
27 | rules: {
28 | 'arrow-body-style': ['error', 'as-needed'],
29 | curly: 'error',
30 |
31 | // I'll probably add some typescript types instead
32 | 'react/prop-types': 'off',
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [smeijer]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1.bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report 🐛
3 | about: Create a bug report for Testing Playground.
4 | labels: bug
5 | ---
6 |
7 |
20 |
21 | ## Bug Report 🐛
22 |
23 |
27 |
28 | ## To Reproduce ✔️
29 |
30 |
38 |
39 | 1. ...step1
40 | 2. ...step2
41 | 3. ...step3
42 |
43 | ## Expected behavior 🤔
44 |
45 |
48 |
49 | ## Suggested solution 🔦
50 |
51 |
55 |
56 | ## Your Environment 💻
57 |
58 | - _browser_: Chrome, Firefox
59 | - _os_: Mac, Windows, Linux
60 | - _any other relevant information_
61 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2.feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request 💄
3 | about: Suggest a new idea for Testing Playground.
4 | labels: feature
5 | ---
6 |
7 |
18 |
19 | ## Summary 💡
20 |
21 |
22 |
23 | ## Examples 🌈
24 |
25 |
28 |
29 | ## Motivation 🔦
30 |
31 |
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | **What**:
20 |
21 |
22 |
23 | **Why**:
24 |
25 |
26 |
27 | **How**:
28 |
29 |
30 |
31 | **Checklist**:
32 |
33 |
34 |
35 |
36 |
37 | - [ ] Tests
38 | - [ ] Ready to be merged
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/.github/scripts/env.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | # We don't want the `tip` tag to affect our changelog, and also don't want it to be used for the nighly version
5 | git tag -d tip &> /dev/null || :
6 |
7 | # PUSHING A TAG TRIGGERS A VERSIONED RELEASE, ANY OTHER PUSH TRIGGERS A NIGHTLY
8 | REF=$(echo "$GITHUB" | jq -r '.ref')
9 | VERSION=$(echo "${REF#refs/tags/}")
10 |
11 | # export some data
12 | set-env () {
13 | echo "$1=$2" >> $GITHUB_ENV
14 | export $1="$2"
15 | }
16 |
17 | set-env "VERSION" "$VERSION"
--------------------------------------------------------------------------------
/.github/scripts/release-notes.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 |
4 | echo "PATH: $(dirname $(realpath $0))"
5 |
6 | : "${ESCAPE:=${GITHUB_ACTIONS:-false}}"
7 |
8 | # generate release notes
9 | CHANGELOG="$(npm run --silent ci:changelog)"
10 |
11 | # escape newlines for github actions
12 | if [ "$ESCAPE" != false ]; then
13 | echo "escape it"
14 | CHANGELOG="${CHANGELOG//'%'/'%25'}"
15 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
16 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
17 | fi
18 |
19 | # export some data
20 | echo "::set-output name=changelog::$CHANGELOG"
21 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | env:
9 | APP_NAME: Testing Playground
10 | NODE_VERSION: '18.x'
11 |
12 | jobs:
13 | create-release:
14 | name: Create Release Notes
15 | runs-on: ubuntu-latest
16 | timeout-minutes: 15
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | with:
21 | fetch-depth: 0
22 |
23 | - name: Fetch all tags
24 | run: git fetch origin +refs/tags/*:refs/tags/*
25 |
26 | - uses: actions/setup-node@v1
27 | with:
28 | node-version: '${{ env.NODE_VERSION }}'
29 |
30 | - name: Cache node modules
31 | id: cache
32 | uses: actions/cache@v1
33 | with:
34 | path: node_modules
35 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
36 |
37 | - name: Install Dependencies
38 | if: steps.cache.outputs.cache-hit != 'true'
39 | run: npm ci
40 | - name: Setup Env
41 | run: |
42 | find .github/scripts -type f -iname "*.sh" -exec chmod +x {} \;
43 | .github/scripts/env.sh
44 | env:
45 | GITHUB: ${{ toJson(github) }}
46 |
47 | - name: Create Release Notes
48 | id: release_notes
49 | run: .github/scripts/release-notes.sh
50 |
51 | - name: Create Release
52 | id: create_release
53 | uses: actions/create-release@v1
54 | with:
55 | tag_name: ${{ github.ref }}
56 | release_name: ${{ env.APP_NAME }} ${{ env.VERSION }}
57 | body: ${{ steps.release_notes.outputs.changelog }}
58 | env:
59 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
60 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 |
7 | env:
8 | NODE_VERSION: '18.x'
9 |
10 | jobs:
11 | setup:
12 | name: Setup
13 | runs-on: ubuntu-latest
14 | timeout-minutes: 5
15 | steps:
16 | - uses: actions/checkout@v2
17 |
18 | - uses: actions/setup-node@v1
19 | with:
20 | node-version: '${{ env.NODE_VERSION }}'
21 |
22 | - name: Cache node modules
23 | id: cache
24 | uses: actions/cache@v1
25 | with:
26 | path: node_modules
27 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
28 |
29 | - name: Install Dependencies
30 | if: steps.cache.outputs.cache-hit != 'true'
31 | run: npm ci
32 |
33 | eslint:
34 | name: Eslint
35 | runs-on: ubuntu-latest
36 | needs: [setup]
37 | timeout-minutes: 5
38 | steps:
39 | - uses: actions/checkout@v2
40 | with:
41 | fetch-depth: 0
42 |
43 | - uses: actions/setup-node@v1
44 | with:
45 | node-version: '${{ env.NODE_VERSION }}'
46 |
47 | - name: Fetch all branches
48 | run: |
49 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
50 |
51 | - name: Cache node modules
52 | uses: actions/cache@v1
53 | with:
54 | path: node_modules
55 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
56 |
57 | - name: Run Eslint
58 | run: npm run ci:lint -- $(git diff --diff-filter d --name-only origin/${{ github.base_ref }}...HEAD -- '*.js' '*.ts' '*.tsx')
59 |
60 | unit-tests:
61 | name: Unit Tests
62 | runs-on: ubuntu-latest
63 | needs: [setup]
64 | timeout-minutes: 5
65 | steps:
66 | - uses: actions/checkout@v2
67 |
68 | - uses: actions/setup-node@v1
69 | with:
70 | node-version: '${{ env.NODE_VERSION }}'
71 |
72 | - name: Cache node modules
73 | uses: actions/cache@v1
74 | with:
75 | path: node_modules
76 | key: ${{ runner.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
77 |
78 | - name: Run Tests
79 | run: npm test
80 | e2e-tests:
81 | name: e2e Tests
82 | runs-on: ubuntu-latest
83 | needs: [setup]
84 | if: false # temporarily disabled
85 | steps:
86 | - name: Checkout
87 | uses: actions/checkout@v2
88 |
89 | - name: Cypress run
90 | uses: cypress-io/github-action@v2
91 | with:
92 | start: npm start
93 | wait-on: http://localhost:5173
94 | command: npm run cypress:run
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | cypress/videos
2 | dist/
3 | node_modules/
4 |
5 | # Local Netlify folder
6 | .netlify
7 | .env
8 |
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '**/*.js': (files) => [
3 | `eslint --quiet --fix ${files.join(' ')}`,
4 | `vitest related --run`,
5 | ],
6 | '**/*.{md,js,json,yml,html,css}': (files) => [
7 | `prettier --write ${files.join(' ')}`,
8 | ],
9 | };
10 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 18.9.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | dist
3 | cypress
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: true,
3 | trailingComma: 'all',
4 | singleQuote: true,
5 | printWidth: 80,
6 | tabWidth: 2,
7 | overrides: [
8 | {
9 | files: '*.html',
10 | options: {
11 | printWidth: 120,
12 | },
13 | },
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/.proxyrc:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://localhost:8888/"
4 | }
5 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at stephan.meijer+coc@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for being willing to contribute!
4 |
5 | **Working on your first Pull Request?** You can learn how from this _free_
6 | series [How to Contribute to an Open Source Project on GitHub][egghead]
7 |
8 | ## Project setup
9 |
10 | 1. Fork and clone the repo
11 | 2. Run `npm ci` to install dependencies
12 | 3. Create a branch for your PR with `git checkout -b feature/your-branch-name`
13 |
14 | > Tip: Keep your `develop` branch pointing at the original repository and make
15 | > pull requests from branches on your fork. To do this, run:
16 | >
17 | > ```
18 | > git remote add upstream git@github.com:testing-library/testing-playground.git
19 | > git fetch upstream
20 | > git branch --set-upstream-to=upstream/develop develop
21 | > ```
22 | >
23 | > This will add the original repository as a "remote" called "upstream," Then
24 | > fetch the git information from that remote, then set your local `develop`
25 | > branch to use the upstream develop branch whenever you run `git pull`. Then you
26 | > can make all of your pull request branches based on this `develop` branch.
27 | > Whenever you want to update your version of `develop`, do a regular `git pull`.
28 |
29 | ## Committing and Pushing changes
30 |
31 | Please make sure to run the tests before you commit your changes. You can run
32 | `npm run test` to do so.
33 |
34 | ### Linting with git hooks
35 |
36 | There are git hooks set up with this project that are automatically installed
37 | when you install dependencies. They're really handy, and will take care of linting
38 | and formatting for you.
39 |
40 | ## Thinking of a new feature?
41 |
42 | Make sure you read our [features guidelines](/docs/features.md).
43 |
44 | ## Help needed
45 |
46 | Please checkout the [the open issues][issues]
47 |
48 | Also, please watch the repo and respond to questions/bug reports/feature
49 | requests! Thanks!
50 |
51 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
52 | [issues]: https://github.com/testing-library/testing-playground/issues
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Stephan Meijer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MAINTAINING.md:
--------------------------------------------------------------------------------
1 | # Maintaining
2 |
3 | This is documentation for maintainers of this project.
4 |
5 | ## Code of Conduct
6 |
7 | Please review, understand, and be an example of it. Violations of the code of
8 | conduct are taken seriously, even (especially) for maintainers.
9 |
10 | ## Issues
11 |
12 | We want to support and build the community. We do that best by helping people
13 | learn to solve their own problems. We have an issue template and hopefully most
14 | folks follow it. If it's not clear what the issue is, invite them to create a
15 | minimal reproduction of what they're trying to accomplish or the bug they think
16 | they've found.
17 |
18 | Once it's determined that a code change is necessary, point people to
19 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a
20 | pull request. If they're the one who needs the feature, they're the one who can
21 | build it. If they need some hand holding and you have time to lend a hand,
22 | please do so. It's an investment into another human being, and an investment
23 | into a potential maintainer.
24 |
25 | Remember that this is open source, so the code is not yours, it's ours. If
26 | someone needs a change in the codebase, you don't have to make it happen
27 | yourself. Commit as much time to the project as you want/need to. Nobody can ask
28 | any more of you than that.
29 |
30 | ## Pull Requests
31 |
32 | As a maintainer, you're fine to make your branches on the main repo or on your
33 | own fork. Either way is fine.
34 |
35 | When we receive a pull request, a various automated checks are kicked off
36 | automatically. We avoid merging anything that doesn't pass these checks.
37 |
38 | Please review PRs and focus on the code rather than the individual. You never
39 | know when this is someone's first ever PR and we want their experience to be as
40 | positive as possible, so be uplifting and constructive.
41 |
42 | When you merge the pull request, 99% of the time you should use the
43 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/)
44 | feature. This keeps our git history clean, but more importantly, this allows us
45 | to make any necessary changes to the commit message so we release what we want
46 | to release. See the next section on Releases for more about that.
47 |
48 | ## Release
49 |
50 | Our releases are automatic. They happen whenever code lands into `live`. A
51 | netlify build gets kicked off, and when tagged a changelog is published on
52 | GitHub. The changelog is generated based on the git commit messages. With this
53 | in mind, **please brush up on [the commit message convention][commit] which
54 | drives our releases.**
55 |
56 | > One important note about this: Please make sure that commit messages do NOT
57 | > contain the words "BREAKING CHANGE" in them unless we want to push a major
58 | > version. I've been burned by this more than once where someone will include
59 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not
60 | > a huge deal honestly, but kind of annoying...
61 |
62 | ## Thanks!
63 |
64 | Thank you so much for helping to maintain this project!
65 |
66 | [commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md
67 |
--------------------------------------------------------------------------------
/ROADMAP.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 |
3 | ## Interactive HTML Preview
4 |
5 | _Status: Shipped at May 23, 2020 ([#2](https://github.com/testing-library/testing-playground/issues/2))_
6 |
7 | The html preview tab should be made interactive. In such a way that one can hover elements and see the recommended query. When clicking on the link, the query should applied to the editor, like the various query buttons do.
8 |
9 | ## Update Readme.md
10 |
11 | _Status: Shipped at May 25, 2020 ([#23](https://github.com/testing-library/testing-playground/pull/23))_
12 |
13 | We don't have any instructions on how to get the thing started at localhost, or how to contribute. This should be made crystal clear.
14 |
15 | _Status: Shipped at May 24, 2020 ([#14](https://github.com/testing-library/testing-playground/issues/4))_
16 |
17 | Also, I have no idea how it works, but I know we need to use emoji-key to list contributors in the readme. Is that an automated thing?
18 |
19 | ## Add tests
20 |
21 | _Status: Ongoing ([#7](https://github.com/testing-library/testing-playground/issues/7))_
22 |
23 | A [testing-playground.com] without tests... I don't think that should be a thing, but at this moment it is.
24 |
25 | We should fix that, and use Vitest & testing-library to do it.
26 |
27 | ## Usage instructions
28 |
29 | _Status: In backlog ([#8](https://github.com/testing-library/testing-playground/issues/8))_
30 |
31 | Do we need some instructions on how to use the tool? I'm not sure about this yet, but if we do, it should be added to the playground itself. Not just a markdown on github.
32 |
33 | ## Embeddable
34 |
35 | _Status: Shipped at May 28, 2020 ([#9](https://github.com/testing-library/testing-playground/issues/9))_
36 |
37 | How awesome would it be if we had an embed-mode, so users can embed the playground in the blogs they write about testing / testing-library?
38 |
39 | ## Support User-Event
40 |
41 | _Status: In backlog ([#10](https://github.com/testing-library/testing-playground/issues/10))_
42 |
43 | I haven't worked out the details, but enabling users to play with [user-event]s would awesome. Perhaps together with adding support for [HTML mixed mode]?
44 |
45 | ## DevTools!
46 |
47 | _Status: In progress ([#11](https://github.com/testing-library/testing-playground/issues/11))_
48 |
49 | I believe we can wrap this project into a chrome extension. That way people can use the thing on their own sites, without needing to copy / paste html fragments. How awesome would that be?!
50 |
51 | [testing-playground.com]: https://testing-playground.com
52 | [html mixed mode]: https://codemirror.net/mode/htmlmixed/index.html
53 | [user-event]: https://testing-library.com/docs/ecosystem-user-event
54 |
--------------------------------------------------------------------------------
/_redirects:
--------------------------------------------------------------------------------
1 | /oembed /.netlify/functions/oembed 200!
2 | /api/* /.netlify/functions/:splat 200!
3 | / /.netlify/functions/server 200!
4 | /* /.netlify/functions/server 200
5 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "baseUrl": "http://localhost:5173",
3 | "integrationFolder": "cypress/e2e"
4 | }
5 |
--------------------------------------------------------------------------------
/cypress/e2e/header.spec.js:
--------------------------------------------------------------------------------
1 | it('renders Testing Playground Header', () => {
2 | cy.visit('/');
3 |
4 | cy.findByRole('heading', {
5 | level: 1,
6 | name: 'Testing Playground mascot Froggy ️ Testing Playground',
7 | }).should('exist');
8 |
9 | cy.findByRole('link', {
10 | name: 'Testing Playground mascot Froggy ️ Testing Playground',
11 | })
12 | .should('exist')
13 | .and('have.attr', 'href', '/');
14 |
15 | cy.findByRole('button', { name: /playground/i }).should('exist');
16 | cy.findByRole('button', { name: /run/i }).should('exist');
17 | cy.findByRole('button', { name: /settings/i }).should('exist');
18 | cy.findByRole('button', { name: /more info/i }).should('exist');
19 |
20 | cy.findByRole('button', { name: /playground/i }).click();
21 | // playground menu shows
22 | cy.findByRole('menuitem', { name: /new/i }).should('exist');
23 | cy.findByRole('menuitem', { name: /save/i }).should('exist');
24 | cy.findByRole('menuitem', { name: /fork/i }).should('exist');
25 | cy.findByRole('menuitem', { name: /share/i }).should('exist');
26 | cy.findByRole('menuitem', { name: /embed/i }).should('exist');
27 |
28 | cy.findByRole('button', { name: /settings/i }).click();
29 | // previous menu disappear
30 | cy.findByRole('menuitem', { name: /new/i }).should('not.exist');
31 | cy.findByRole('menuitem', { name: /save/i }).should('not.exist');
32 | cy.findByRole('menuitem', { name: /fork/i }).should('not.exist');
33 | cy.findByRole('menuitem', { name: /share/i }).should('not.exist');
34 | cy.findByRole('menuitem', { name: /embed/i }).should('not.exist');
35 |
36 | // settings menu shows
37 | cy.findByLabelText(/auto-run code/i).should('exist');
38 | cy.findByLabelText(/test-id attribute:/i).should('exist');
39 |
40 | cy.findByRole('button', { name: /more info/i }).click();
41 | // previous menu disappear
42 | cy.findByLabelText(/auto-run code/i).should('not.be.visible');
43 | cy.findByLabelText(/test-id attribute:/i).should('not.be.visible');
44 |
45 | // more info menu shows
46 | cy.findByRole('menuitem', { name: /github/i }).should('exist').and('have.attr', 'href', 'https://github.com/testing-library/testing-playground/issues');
47 | cy.findByRole('menuitem', { name: /support us/i }).should('exist').and('have.attr', 'href', 'https://github.com/sponsors/smeijer');
48 | cy.findByRole('menuitem', { name: /twitter/i }).should('exist').and('have.attr', 'href', 'https://twitter.com/meijer_s');
49 | cy.findByRole('menuitem', { name: /chrome extension/i }).should('exist').and('have.attr', 'href', 'https://chrome.google.com/webstore/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano');
50 | cy.findByRole('menuitem', { name: /introduction/i }).should('exist').and('have.attr', 'href', 'https://testing-library.com/docs/dom-testing-library/intro');
51 | cy.findByRole('menuitem', { name: /query priority/i }).should('exist').and('have.attr', 'href', 'https://testing-library.com/docs/guide-which-query');
52 | cy.findByRole('menuitem', { name: /common mistakes/i }).should('exist').and('have.attr', 'href', 'https://kentcdodds.com/blog/common-mistakes-with-react-testing-library');
53 |
54 | // click outside
55 | cy.findByRole('button', { name: /more info/i }).parent().click();
56 | // previous menu disappear
57 | cy.findByRole('menuitem', { name: /github/i }).should('not.exist');
58 | cy.findByRole('menuitem', { name: /support us/i }).should('not.exist');
59 | cy.findByRole('menuitem', { name: /twitter/i }).should('not.exist');
60 | cy.findByRole('menuitem', { name: /chrome extension/i }).should('not.exist');
61 | cy.findByRole('menuitem', { name: /introduction/i }).should('not.exist');
62 | cy.findByRole('menuitem', { name: /query priority/i }).should('not.exist');
63 | cy.findByRole('menuitem', { name: /common mistakes/i }).should('not.exist');
64 | });
65 |
--------------------------------------------------------------------------------
/cypress/e2e/initApp.spec.js:
--------------------------------------------------------------------------------
1 | it('renders Testing Playground with an example', () => {
2 | cy.visit('/');
3 |
4 | cy.getMarkupEditor().should('exist');
5 |
6 | cy.getSandboxBody().within(() => {
7 | cy.findByLabelText('Email address')
8 | .should('exist')
9 | .and('have.attr', 'type', 'email')
10 | .and('have.attr', 'placeholder', 'Enter email');
11 |
12 | cy.findByText("It's safe with us. We hate spam!").should('exist');
13 |
14 | cy.findByLabelText('Password')
15 | .should('exist')
16 | .and('have.attr', 'type', 'password')
17 | .and('have.attr', 'placeholder', 'Password');
18 |
19 | cy.findByLabelText('I accept the terms and conditions')
20 | .should('exist')
21 | .and('have.attr', 'type', 'checkbox')
22 | .and('not.be.checked');
23 |
24 | cy.findByRole('link', { name: 'terms and conditions' })
25 | .should('exist')
26 | .and('have.attr', 'href', 'https://www.example.com');
27 |
28 | cy.findByRole('button', { name: 'Submit' }).should('exist');
29 | });
30 |
31 | cy.findByRole('region', { name: 'html preview' }).within(() => {
32 | cy.findByRole('button', { name: 'expand' }).should('exist');
33 | cy.findByText('accessible roles:').should('exist');
34 | cy.findByText('generic').should('exist');
35 | });
36 |
37 | cy.getQueryEditor().should('exist');
38 |
39 | cy.findByRole('region', { name: 'query suggestion' }).within(() => {
40 | cy.findByText('> ').should('exist');
41 | cy.findByRole('button', { name: 'expand' }).should('exist');
42 | });
43 |
44 | cy.getResult().within(() => {
45 | cy.findByText('suggested query').should('exist');
46 |
47 | cy.findByText("> getByRole('button', { name: /submit/i })").should('exist');
48 |
49 | cy.findByRole('button', { name: 'copy query' }).should('exist');
50 |
51 | cy.findByText(
52 | 'There is one thing though. You could make the query a bit more specific by adding the name option.',
53 | ).should('exist');
54 |
55 | cy.findByRole('heading', {
56 | level: 3,
57 | name: '1. Queries Accessible to Everyone',
58 | }).should('exist');
59 |
60 | cy.findByRole('heading', {
61 | level: 3,
62 | name: '2. Semantic Queries',
63 | }).should('exist');
64 |
65 | cy.findByRole('heading', {
66 | level: 3,
67 | name: '3. Test IDs',
68 | }).should('exist');
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/cypress/add-commands';
2 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 |
19 | // Add common command to get the body of the sandbox iframe
20 | // See: https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
21 | Cypress.Commands.add('getSandboxBody', () => {
22 | cy.log('getSandboxBody');
23 |
24 | return cy
25 | .findByTitle('sandbox')
26 | .its('0.contentDocument.body', { log: false })
27 | .should('not.be.empty')
28 | .then((body) => cy.wrap(body, { log: false }));
29 | });
30 |
31 | // Add commands to clear CodeMirror editors
32 | // See: https://stackoverflow.com/questions/62012319/how-can-i-clear-a-codemirror-editor-field-from-cypress
33 |
34 | Cypress.Commands.add('clearMarkupEditor', () => {
35 | cy.log('clearMarkupEditor');
36 |
37 | cy.get('.CodeMirror', { log: false })
38 | .first({ log: false })
39 | .its('0.CodeMirror', { log: false })
40 | .then((editor) => {
41 | editor.setValue('');
42 | });
43 | });
44 |
45 | Cypress.Commands.add('clearQueryEditor', () => {
46 | cy.log('clearQueryEditor');
47 |
48 | cy.get('.CodeMirror', { log: false })
49 | .last({ log: false })
50 | .its('0.CodeMirror', { log: false })
51 | .then((editor) => {
52 | editor.setValue('');
53 | });
54 | });
55 |
56 | Cypress.Commands.add('getMarkupEditor', () => {
57 | cy.log('getMarkupEditor');
58 |
59 | return cy.get('.CodeMirror textarea', { log: false }).first({ log: false });
60 | });
61 |
62 | Cypress.Commands.add('getQueryEditor', () => {
63 | cy.log('getQueryEditor');
64 |
65 | return cy.get('.CodeMirror textarea', { log: false }).last({ log: false });
66 | });
67 |
68 | Cypress.Commands.add('getResult', () => {
69 | cy.log('getResult');
70 |
71 | return cy.get('div[data-testid=result]', { log: false });
72 | });
73 |
--------------------------------------------------------------------------------
/devtools/src/background/background.js:
--------------------------------------------------------------------------------
1 | // This import needs to be here, we don't use the bridge here directly. But
2 | // simply importing crx-bridge, is what creates the messaging proxy.
3 | import 'crx-bridge';
4 |
--------------------------------------------------------------------------------
/devtools/src/content-script/content-script.js:
--------------------------------------------------------------------------------
1 | import Bridge from 'crx-bridge';
2 | import setupHighlighter from './highlighter';
3 |
4 | import parser from '../../../src/parser';
5 | import { getAllPossibleQueries } from '../../../src/lib';
6 | import inject from './lib/inject';
7 | import { setup } from '../window/testing-library';
8 | import onDocReady from './lib/onDocReady';
9 |
10 | // HACK: mock out console warn for https://github.com/testing-library/testing-playground/issues/330
11 | console.warn = () => undefined;
12 |
13 | function init() {
14 | inject('../window/testing-library.js');
15 | setup(window);
16 |
17 | window.__TESTING_PLAYGROUND__ = window.__TESTING_PLAYGROUND__ || {};
18 | const hook = window.__TESTING_PLAYGROUND__;
19 |
20 | hook.highlighter = setupHighlighter({ view: window, onSelectNode });
21 |
22 | function onSelectNode(node) {
23 | const queries = getAllPossibleQueries(
24 | {
25 | rootNode: document.body,
26 | element: node,
27 | },
28 | 'DEVTOOLS',
29 | );
30 |
31 | const suggestion = Object.values(queries).find(Boolean);
32 |
33 | const result = parser.parse(
34 | {
35 | rootNode: document.body,
36 | query: suggestion?.snippet || '',
37 | },
38 | 'DEVTOOLS',
39 | );
40 |
41 | Bridge.sendMessage('SELECT_NODE', result, 'devtools');
42 | }
43 |
44 | Bridge.onMessage('PARSE_QUERY', function ({ data }) {
45 | const result = parser.parse({
46 | rootNode: document.body,
47 | query: data.query,
48 | });
49 |
50 | if (data.highlight) {
51 | const selector = result.elements.map((x) => x.cssPath).join(', ');
52 | const nodes =
53 | result.elements.length > 0
54 | ? Array.from(document.body.querySelectorAll(selector))
55 | : [];
56 |
57 | hook.highlighter.highlight({
58 | nodes,
59 | hideAfterTimeout: data.hideAfterTimeout ?? 1500,
60 | });
61 | }
62 |
63 | return { result };
64 | });
65 |
66 | Bridge.onMessage('SET_SETTINGS', function ({ data }) {
67 | parser.configure(data);
68 | });
69 |
70 | // when the selected element is changed by using the element inspector,
71 | // this method will be called from devtools/main.js
72 | hook.onSelectionChanged = function onSelectionChanged(el) {
73 | onSelectNode(el);
74 | };
75 | }
76 |
77 | onDocReady(init);
78 |
--------------------------------------------------------------------------------
/devtools/src/content-script/highlighter/Highlighter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | * Copyright (c) 2020, Stephan Meijer
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | **/
9 |
10 | import Overlay from './Overlay';
11 |
12 | const SHOW_DURATION = 2000;
13 |
14 | let timeoutID = null;
15 | let overlay = null;
16 |
17 | export function hideOverlay() {
18 | timeoutID = null;
19 |
20 | if (overlay !== null) {
21 | overlay.remove();
22 | overlay = null;
23 | }
24 | }
25 |
26 | export function showOverlay(elements, hideAfterTimeout) {
27 | if (window.document == null) {
28 | return;
29 | }
30 |
31 | if (timeoutID !== null) {
32 | clearTimeout(timeoutID);
33 | }
34 |
35 | if (elements == null) {
36 | return;
37 | }
38 |
39 | if (overlay === null) {
40 | overlay = new Overlay();
41 | }
42 |
43 | overlay.inspect(elements);
44 |
45 | if (hideAfterTimeout) {
46 | timeoutID = setTimeout(hideOverlay, SHOW_DURATION);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/devtools/src/content-script/highlighter/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | * Copyright (c) 2020, Stephan Meijer
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE file in the root directory of this source tree.
7 | *
8 | **/
9 |
10 | // Get the window object for the document that a node belongs to,
11 | // or return null if it cannot be found (node not attached to DOM,
12 | // etc).
13 | export function getOwnerWindow(node) {
14 | return node.ownerDocument?.defaultView || null;
15 | }
16 |
17 | // Get the iframe containing a node, or return null if it cannot
18 | // be found (node not within iframe, etc).
19 | export function getOwnerIframe(node) {
20 | return getOwnerWindow(node)?.frameElement || null;
21 | }
22 |
23 | // Get a bounding client rect for a node, with an
24 | // offset added to compensate for its border.
25 | export function getBoundingClientRectWithBorderOffset(node) {
26 | const dimensions = getElementDimensions(node);
27 |
28 | return mergeRectOffsets([
29 | node.getBoundingClientRect(),
30 | {
31 | top: dimensions.borderTop,
32 | left: dimensions.borderLeft,
33 | bottom: dimensions.borderBottom,
34 | right: dimensions.borderRight,
35 | // This width and height won't get used by mergeRectOffsets (since this
36 | // is not the first rect in the array), but we set them so that this
37 | // object typechecks as a ClientRect.
38 | width: 0,
39 | height: 0,
40 | },
41 | ]);
42 | }
43 |
44 | // Add together the top, left, bottom, and right properties of
45 | // each ClientRect, but keep the width and height of the first one.
46 | export function mergeRectOffsets(rects) {
47 | return rects.reduce((previousRect, rect) => {
48 | if (previousRect == null) {
49 | return rect;
50 | }
51 |
52 | return {
53 | top: previousRect.top + rect.top,
54 | left: previousRect.left + rect.left,
55 | width: previousRect.width,
56 | height: previousRect.height,
57 | bottom: previousRect.bottom + rect.bottom,
58 | right: previousRect.right + rect.right,
59 | };
60 | });
61 | }
62 |
63 | // Calculate a boundingClientRect for a node relative to boundaryWindow,
64 | // taking into account any offsets caused by intermediate iframes.
65 | export function getNestedBoundingClientRect(node, boundaryWindow) {
66 | const ownerIframe = getOwnerIframe(node);
67 |
68 | if (ownerIframe && ownerIframe.contentWindow !== boundaryWindow) {
69 | const rects = [node.getBoundingClientRect()];
70 | let currentIframe = ownerIframe;
71 | let onlyOneMore = false;
72 | while (currentIframe) {
73 | const rect = getBoundingClientRectWithBorderOffset(currentIframe);
74 | rects.push(rect);
75 | currentIframe = getOwnerIframe(currentIframe);
76 |
77 | if (onlyOneMore) {
78 | break;
79 | }
80 | // We don't want to calculate iframe offsets upwards beyond
81 | // the iframe containing the boundaryWindow, but we
82 | // need to calculate the offset relative to the boundaryWindow.
83 | if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
84 | onlyOneMore = true;
85 | }
86 | }
87 |
88 | return mergeRectOffsets(rects);
89 | } else {
90 | return node.getBoundingClientRect();
91 | }
92 | }
93 |
94 | export function getElementDimensions(domElement) {
95 | const calculatedStyle = window.getComputedStyle(domElement);
96 |
97 | return {
98 | borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
99 | borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
100 | borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
101 | borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
102 | marginLeft: parseInt(calculatedStyle.marginLeft, 10),
103 | marginRight: parseInt(calculatedStyle.marginRight, 10),
104 | marginTop: parseInt(calculatedStyle.marginTop, 10),
105 | marginBottom: parseInt(calculatedStyle.marginBottom, 10),
106 | paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
107 | paddingRight: parseInt(calculatedStyle.paddingRight, 10),
108 | paddingTop: parseInt(calculatedStyle.paddingTop, 10),
109 | paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
110 | };
111 | }
112 |
--------------------------------------------------------------------------------
/devtools/src/content-script/lib/inject.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 |
3 | function inject(src) {
4 | return new Promise((resolve) => {
5 | const target = document.head || document.documentElement;
6 |
7 | const script = document.createElement('script');
8 | script.setAttribute('type', 'text/javascript');
9 | script.setAttribute(
10 | 'src',
11 | src.includes('://') ? src : chrome.runtime.getURL(src),
12 | );
13 | script.addEventListener('load', () => {
14 | target.removeChild(script);
15 | resolve();
16 | });
17 |
18 | target.appendChild(script);
19 | });
20 | }
21 |
22 | export default inject;
23 |
--------------------------------------------------------------------------------
/devtools/src/content-script/lib/onDocReady.js:
--------------------------------------------------------------------------------
1 | function onDocReady(fn) {
2 | if (document.readyState !== 'loading') {
3 | return fn();
4 | }
5 |
6 | setTimeout(() => {
7 | onDocReady(fn);
8 | }, 9);
9 | }
10 |
11 | export default onDocReady;
12 |
--------------------------------------------------------------------------------
/devtools/src/devtools/components/Icons/InspectIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function InspectIcon() {
4 | return (
5 |
12 | );
13 | }
14 |
15 | export default InspectIcon;
16 |
--------------------------------------------------------------------------------
/devtools/src/devtools/components/Icons/LayersIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function LayersIcon() {
4 | return (
5 |
11 | );
12 | }
13 |
14 | export default LayersIcon;
15 |
--------------------------------------------------------------------------------
/devtools/src/devtools/components/Icons/LogIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function LogIcon() {
4 | return (
5 |
12 | );
13 | }
14 |
15 | export default LogIcon;
16 |
--------------------------------------------------------------------------------
/devtools/src/devtools/components/Icons/SelectIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function SelectIcon() {
4 | return (
5 |
12 | );
13 | }
14 |
15 | export default SelectIcon;
16 |
--------------------------------------------------------------------------------
/devtools/src/devtools/components/Icons/SettingsIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function SettingsIcon() {
4 | return (
5 |
11 | );
12 | }
13 |
14 | export default SettingsIcon;
15 |
--------------------------------------------------------------------------------
/devtools/src/devtools/components/MenuBar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Bridge from 'crx-bridge';
3 |
4 | import inspectedWindow from '../lib/inspectedWindow';
5 |
6 | // we don't use octicons here, as that style doesn't really fit in devtools
7 | import SelectIcon from './Icons/SelectIcon';
8 | import LayersIcon from './Icons/LayersIcon';
9 | import InspectIcon from './Icons/InspectIcon';
10 | import SettingsIcon from './Icons/SettingsIcon';
11 | import LogIcon from './Icons/LogIcon';
12 | import { Menu, MenuButton, MenuPopover } from '../../../../src/components/Menu';
13 | import Settings from '../../../../src/components/Settings';
14 | import { getSettings, setSettings } from '../lib/settings';
15 |
16 | function MenuBar({ cssPath, suggestion }) {
17 | return (
18 |
19 |
28 |
29 |
38 |
39 |
40 |
41 |
57 |
58 |
66 |
67 |
75 |
76 | );
77 | }
78 |
79 | export default MenuBar;
80 |
--------------------------------------------------------------------------------
/devtools/src/devtools/lib/inspectedWindow.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 |
3 | // We can't do this with messaging, because in Chrome, eval always runs in the
4 | // context of the ContentScript, not in the context of Window. Maybe we can just
5 | // do something with `useContentScriptContext: true`, maintain a log on the most
6 | // recent(ly) used element(s), assign an id to them, and then use messaging. But
7 | // for now, this is way easier.
8 |
9 | function escape(str) {
10 | return str.replace(/'/g, "\\'").replace(/\n/g, '\\n');
11 | }
12 |
13 | function logQuery(query) {
14 | chrome.devtools.inspectedWindow.eval(`
15 | console.log('${escape(query)}');
16 | console.log(eval(${query}));
17 | `);
18 | }
19 |
20 | function inspect(cssPath) {
21 | chrome.devtools.inspectedWindow.eval(`
22 | inspect(document.querySelector('${cssPath}'));
23 | `);
24 | }
25 |
26 | export default {
27 | logQuery,
28 | inspect,
29 | };
30 |
--------------------------------------------------------------------------------
/devtools/src/devtools/lib/settings.js:
--------------------------------------------------------------------------------
1 | import Bridge from 'crx-bridge';
2 |
3 | const localSettings = navigator.cookieEnabled
4 | ? JSON.parse(localStorage.getItem('playground_settings'))
5 | : {};
6 |
7 | let _settings = Object.assign(
8 | {
9 | testIdAttribute: 'data-testid',
10 | },
11 | localSettings,
12 | );
13 |
14 | Bridge.sendMessage('SET_SETTINGS', _settings, 'content-script');
15 |
16 | export function getSettings() {
17 | return _settings;
18 | }
19 |
20 | export function setSettings(settings) {
21 | Object.assign(_settings, settings);
22 | Bridge.sendMessage('SET_SETTINGS', _settings, 'content-script');
23 | if (navigator.cookieEnabled) {
24 | localStorage.setItem('playground_settings', JSON.stringify(_settings));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/devtools/src/devtools/lib/utils.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 | const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
3 |
4 | export function getBrowserName() {
5 | return IS_CHROME ? 'Chrome' : 'Firefox';
6 | }
7 |
8 | export function getBrowserTheme() {
9 | if (!chrome.devtools || !chrome.devtools.panels) {
10 | return 'light';
11 | }
12 |
13 | return chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light';
14 | }
15 |
--------------------------------------------------------------------------------
/devtools/src/devtools/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/devtools/src/devtools/main.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 | import { getBrowserName } from './lib/utils';
3 | const panels = chrome.devtools.panels;
4 |
5 | const isChrome = getBrowserName() === 'Chrome';
6 | const name = isChrome ? '🐸 Testing Playground' : 'Testing Playground';
7 |
8 | panels.create(name, '', '/devtools/panel.html');
9 |
10 | panels.elements.createSidebarPane(name, (sidebar) =>
11 | sidebar.setPage('/devtools/pane.html'),
12 | );
13 |
14 | function onSelectionChanged() {
15 | chrome.devtools.inspectedWindow.eval(
16 | '__TESTING_PLAYGROUND__.onSelectionChanged($0)',
17 | {
18 | useContentScriptContext: true,
19 | },
20 | );
21 | }
22 |
23 | panels.elements.onSelectionChanged.addListener(onSelectionChanged);
24 |
25 | onSelectionChanged();
26 |
--------------------------------------------------------------------------------
/devtools/src/devtools/pane.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/devtools/src/devtools/pane.js:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime';
2 | import React, { useState, useEffect } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import Bridge from 'crx-bridge';
5 | import Result from '../../../src/components/Result';
6 | import inspectedWindow from './lib/inspectedWindow';
7 |
8 | function Panel() {
9 | const [result, setResult] = useState({});
10 |
11 | useEffect(() => {
12 | Bridge.onMessage('SELECT_NODE', ({ data }) => {
13 | setResult(data);
14 | });
15 | }, [setResult]);
16 |
17 | const dispatch = (action) => {
18 | switch (action.type) {
19 | case 'SET_QUERY': {
20 | inspectedWindow.logQuery(action.query);
21 | break;
22 | }
23 | }
24 | };
25 |
26 | return (
27 |
28 | {result && }
29 |
30 | );
31 | }
32 |
33 | ReactDOM.render(, document.getElementById('app'));
34 |
--------------------------------------------------------------------------------
/devtools/src/devtools/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/devtools/src/devtools/panel.js:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime';
2 | import React, { useState, useCallback, useEffect, useRef } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import Bridge from 'crx-bridge';
5 | import Query from '../../../src/components/Query';
6 | import Result from '../../../src/components/Result';
7 | import MenuBar from './components/MenuBar';
8 |
9 | function Panel() {
10 | const [result, setResult] = useState({});
11 | const editor = useRef(null);
12 |
13 | useEffect(() => {
14 | Bridge.onMessage('SELECT_NODE', ({ data }) => {
15 | setResult(data);
16 | editor.current.setValue(data.query);
17 | });
18 | }, [setResult]);
19 |
20 | const dispatch = useCallback(
21 | (action) => {
22 | switch (action.type) {
23 | case 'SET_QUERY': {
24 | Bridge.sendMessage(
25 | 'PARSE_QUERY',
26 | {
27 | query: action.query,
28 | highlight: true,
29 | },
30 | 'content-script',
31 | ).then(({ result }) => {
32 | setResult(result);
33 | });
34 |
35 | if (action.origin !== 'EDITOR') {
36 | editor.current.setValue(action.query);
37 | }
38 | break;
39 | }
40 |
41 | case 'SET_QUERY_EDITOR': {
42 | editor.current = action.editor;
43 | }
44 | }
45 | },
46 | [setResult],
47 | );
48 |
49 | return (
50 |
51 |
52 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | ReactDOM.render(, document.getElementById('app'));
76 |
--------------------------------------------------------------------------------
/devtools/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Testing Playground",
4 | "description": "Simple and complete DOM testing playground that encourage good testing practices.",
5 | "version": "1.16.0",
6 | "version_name": "1.16.3",
7 | "minimum_chrome_version": "58",
8 | "icons": {
9 | "16": "icons/16-production.png",
10 | "32": "icons/32-production.png",
11 | "48": "icons/48-production.png",
12 | "128": "icons/128-production.png"
13 | },
14 | "browser_action": {
15 | "default_icon": {
16 | "16": "icons/16-production.png",
17 | "32": "icons/32-production.png",
18 | "48": "icons/48-production.png",
19 | "128": "icons/128-production.png"
20 | }
21 | },
22 | "web_accessible_resources": ["window/testing-library.js"],
23 | "devtools_page": "devtools/main.html",
24 | "content_security_policy": "script-src 'self' 'unsafe-eval' 'sha256-6UcmjVDygSSU8p+3s7E7Kz8EG/ARhPADPRUm9P90HLM='; object-src 'self'",
25 | "background": {
26 | "scripts": ["background/background.js"],
27 | "persistent": false
28 | },
29 | "permissions": ["clipboardWrite"],
30 | "content_scripts": [
31 | {
32 | "matches": [""],
33 | "js": ["content-script/content-script.js"],
34 | "run_at": "document_start"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/devtools/src/window/testing-library.js:
--------------------------------------------------------------------------------
1 | import {
2 | screen,
3 | within,
4 | getSuggestedQuery,
5 | fireEvent,
6 | getRoles,
7 | } from '@testing-library/dom';
8 | import userEvent from '@testing-library/user-event';
9 |
10 | window.__TESTING_PLAYGROUND__ = window.__TESTING_PLAYGROUND__ || {};
11 |
12 | function augmentQuery(query) {
13 | return (...args) => {
14 | const result = query(...args);
15 |
16 | // Promise.resolve(result).then((x) => {
17 | // if (x.nodeType) {
18 | // window.inspect(x);
19 | // }
20 | // });
21 |
22 | return result;
23 | };
24 | }
25 |
26 | export function setup(view) {
27 | // monkey patch `screen` to add testing library to console
28 | for (const prop of Object.keys(screen)) {
29 | view.screen[prop] = view.screen[prop] || augmentQuery(screen[prop]);
30 | view[prop] = view.screen[prop];
31 | }
32 |
33 | view.getRoles = getRoles;
34 | view.fireEvent = fireEvent;
35 | view.getSuggestedQuery = getSuggestedQuery;
36 | view.within = within;
37 |
38 | view.container = view.document.body;
39 | view.userEvent = userEvent;
40 | view.user = userEvent;
41 | }
42 |
43 | setup(window);
44 |
--------------------------------------------------------------------------------
/docs/features.md:
--------------------------------------------------------------------------------
1 | ## Contributing with a new feature
2 |
3 | If you are thinking about adding a new feature to [Testing Playground](https://testing-playground.com/) and wondering where to start, you are in the right place.
4 |
5 | First, make sure that doesn't exist yet as [feature request](https://github.com/testing-library/testing-playground/issues?q=is%3Aissue+is%3Aopen+label%3Afeature). If it doesn't, go ahead and [create it](https://github.com/testing-library/testing-playground/issues/new?labels=feature&template=2.feature.md).
6 |
7 | We will advise you to first gather some feedback from the community and then proceed with coding.
8 |
9 | The project is divided on two applications:
10 |
11 | - [The website](https://testing-playground.com/) (in `src/`)
12 | - [The Extension](https://chrome.google.com/webstore/detail/testing-playground/hejbmebodbijjdhflfknehhcgaklhano?hl=en) (in `devtools/`)
13 |
14 | Have a look at the code to get the big picture, and follow the patterns we are currently using (naming conventions, folder structure...etc.).
15 |
16 | This is how our component hierarchy looks like right now:
17 |
18 | 
19 |
20 | ## You should also know...
21 |
22 | - We use [Taildwind](https://tailwindcss.com/) for CSS.
23 | - We use [ReachUI](https://reach.tech/) for interactive UI elements
24 | - We use [Octicons](https://primer.style/octicons/) icons
25 | - We use [undraw](https://undraw.co/illustrations) for illustrations
26 | - With color `#EDF2F7` and `.5` opacity.
27 |
--------------------------------------------------------------------------------
/docs/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/docs/icon.png
--------------------------------------------------------------------------------
/docs/testing-playground-com.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/docs/testing-playground-com.gif
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "npm run build:public && npm run build:lambda"
3 | functions = "./dist/server"
4 | publish = "./dist/public"
5 |
6 | [dev]
7 | command = "npm run dev:public"
8 | functions = "./src/lambda"
9 | autoLaunch = false
10 | publish = "."
11 |
12 | [template.environment]
13 | GITHUB_GIST_TOKEN = "set token that has gist access"
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Testing Playground",
3 | "version": "1.16.0",
4 | "description": "Simple and complete DOM testing playground that encourage good testing practices.",
5 | "author": "Stephan Meijer ",
6 | "homepage": "https://testing-playground.com",
7 | "license": "MIT",
8 | "private": true,
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/testing-library/testing-playground"
12 | },
13 | "bugs": {
14 | "url": "https://github.com/testing-library/testing-playground/issues"
15 | },
16 | "scripts": {
17 | "dev": "netlify dev",
18 | "dev:public": "vite",
19 | "start": "run-s build:* start:*",
20 | "start:netlify": "netlify dev",
21 | "start:public": "cross-env NODE_ENV=production vite preview",
22 | "build": "run-s clean build:*",
23 | "build:public": "cross-env NODE_ENV=production vite build",
24 | "build:lambda": "cross-env NODE_ENV=production tsx ./scripts/build-lambda.mts",
25 | "build:chromium": "cross-env NODE_ENV=production tsx ./scripts/build-extension.mts --target chromium",
26 | "build:firefox": "cross-env NODE_ENV=production tsx ./scripts/build-extension.mts --target firefox-desktop",
27 | "lint": "run-s lint:*",
28 | "lint:eslint": "eslint . --quiet --fix",
29 | "lint:prettier": "prettier . --write",
30 | "clean": "run-p clean:*",
31 | "clean:cache": "rimraf ./.cache",
32 | "clean:dist": "rimraf ./dist",
33 | "ci:lint": "eslint",
34 | "ci:changelog": "node scripts/changelog.js",
35 | "test": "vitest",
36 | "cypress:open": "cypress open",
37 | "cypress:run": "cypress run"
38 | },
39 | "dependencies": {
40 | "@primer/octicons-react": "^10.1.0",
41 | "@reach/dialog": "^0.10.5",
42 | "@reach/menu-button": "^0.12.1",
43 | "@testing-library/dom": "^9.3.0",
44 | "@testing-library/user-event": "^14.4.3",
45 | "codemirror": "5.54.0",
46 | "crx-bridge": "^2.1.0",
47 | "deep-diff": "^1.0.2",
48 | "dom-accessibility-api": "^0.4.7",
49 | "isomorphic-fetch": "^2.2.1",
50 | "js-beautify": "^1.13.0",
51 | "lodash.debounce": "4.0.8",
52 | "lodash.throttle": "^4.1.1",
53 | "lz-string": "^1.4.4",
54 | "memoize-one": "^5.1.1",
55 | "pretty-format": "26.0.1",
56 | "query-string": "^6.13.7",
57 | "react": "^16.14.0",
58 | "react-custom-scrollbars": "^4.2.1",
59 | "react-dom": "^16.14.0",
60 | "react-router-dom": "^5.2.0",
61 | "react-toastify": "^6.2.0",
62 | "react-toggle": "^4.1.1",
63 | "react-virtualized-auto-sizer": "^1.0.2",
64 | "react-window": "^1.8.6",
65 | "rollup-plugin-node-polyfills": "^0.2.1",
66 | "use-effect-reducer": "^0.6.1"
67 | },
68 | "devDependencies": {
69 | "@babel/preset-react": "^7.18.6",
70 | "@testing-library/cypress": "^7.0.2",
71 | "@testing-library/jest-dom": "^5.11.6",
72 | "@testing-library/react": "^10.4.9",
73 | "@testing-library/react-hooks": "^3.7.0",
74 | "@types/fs-extra": "^9.0.4",
75 | "@types/node": "^20.1.4",
76 | "@typescript-eslint/parser": "^5.59.6",
77 | "@vitejs/plugin-react": "^4.0.0",
78 | "chrome-launch": "^1.1.4",
79 | "conventional-changelog": "^3.1.24",
80 | "conventional-changelog-config-spec": "^2.1.0",
81 | "cross-env": "^7.0.3",
82 | "cypress": "^6.0.1",
83 | "eslint": "^7.15.0",
84 | "eslint-config-prettier": "^6.15.0",
85 | "eslint-plugin-cypress": "^2.11.2",
86 | "eslint-plugin-prettier": "^3.2.0",
87 | "eslint-plugin-react": "^7.21.5",
88 | "eslint-plugin-react-hooks": "^4.2.0",
89 | "eslint-plugin-vitest-globals": "^1.3.1",
90 | "fs-extra": "^9.0.1",
91 | "get-port": "^5.1.1",
92 | "git-semver-tags": "^4.1.1",
93 | "husky": "^4.3.5",
94 | "jest-extended": "^0.11.5",
95 | "lint-staged": "^10.5.3",
96 | "netlify-cli": "^15.1.1",
97 | "npm-run-all": "^4.1.5",
98 | "postcss-import": "^15.1.0",
99 | "postcss-modules": "^6.0.0",
100 | "prettier": "^2.2.1",
101 | "prettier-plugin-tailwindcss": "^0.3.0",
102 | "react-test-renderer": "^16.14.0",
103 | "rimraf": "^5.0.1",
104 | "tailwindcss": "^1.9.6",
105 | "tsx": "^3.12.7",
106 | "vite": "^4.3.9",
107 | "vitest": "^0.31.1",
108 | "web-ext": "^5.5.0",
109 | "workbox-build": "^5.1.4",
110 | "zx": "^7.2.2"
111 | },
112 | "keywords": [],
113 | "husky": {
114 | "hooks": {
115 | "pre-commit": "lint-staged"
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const tailwindcss = require('tailwindcss');
2 | const atImport = require('postcss-import');
3 |
4 | const IS_PRODUCTION = process.env.NODE_ENV === 'production';
5 |
6 | const plugins = [atImport, tailwindcss];
7 |
8 | if (IS_PRODUCTION) {
9 | const purgecss = require('@fullhuman/postcss-purgecss');
10 |
11 | class TailwindExtractor {
12 | static extract(content) {
13 | return content.match(/[A-z0-9-:/]+/g) || [];
14 | }
15 | }
16 |
17 | plugins.push(
18 | purgecss({
19 | content: [
20 | 'src/*.html',
21 | 'src/**/*.js',
22 | 'devtools/**/*.js',
23 | 'devtools/**/*.html',
24 | ],
25 | whitelist: ['body', /CodeMirror/, /react-toggle/, /data-reach/],
26 | whitelistPatternsChildren: [
27 | /CodeMirror/,
28 | /cm-s-dracula/,
29 | /react-toggle/,
30 | /data-reach/,
31 | ],
32 | defaultExtractor: TailwindExtractor.extract,
33 | }),
34 | );
35 | }
36 |
37 | module.exports = { plugins };
38 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/public/code_thinking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/code_thinking.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/favicon.ico
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icon.png
--------------------------------------------------------------------------------
/public/icons/114-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/114-production.png
--------------------------------------------------------------------------------
/public/icons/120-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/120-production.png
--------------------------------------------------------------------------------
/public/icons/128-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/128-production.png
--------------------------------------------------------------------------------
/public/icons/144-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/144-production.png
--------------------------------------------------------------------------------
/public/icons/150-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/150-production.png
--------------------------------------------------------------------------------
/public/icons/152-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/152-production.png
--------------------------------------------------------------------------------
/public/icons/16-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/16-production.png
--------------------------------------------------------------------------------
/public/icons/180-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/180-production.png
--------------------------------------------------------------------------------
/public/icons/192-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/192-production.png
--------------------------------------------------------------------------------
/public/icons/310-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/310-production.png
--------------------------------------------------------------------------------
/public/icons/32-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/32-production.png
--------------------------------------------------------------------------------
/public/icons/36-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/36-production.png
--------------------------------------------------------------------------------
/public/icons/48-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/48-production.png
--------------------------------------------------------------------------------
/public/icons/57-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/57-production.png
--------------------------------------------------------------------------------
/public/icons/60-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/60-production.png
--------------------------------------------------------------------------------
/public/icons/70-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/70-production.png
--------------------------------------------------------------------------------
/public/icons/72-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/72-production.png
--------------------------------------------------------------------------------
/public/icons/76-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/76-production.png
--------------------------------------------------------------------------------
/public/icons/96-production.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/icons/96-production.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Testing Playground",
3 | "description": "Simple and complete DOM testing playground that encourage good testing practices.",
4 | "lang": "en",
5 | "theme_color": "#ffffff",
6 | "background_color": "#ffffff",
7 | "display": "standalone",
8 | "orientation": "any",
9 | "start_url": "/",
10 | "icons": [
11 | {
12 | "src": "icons/36-production.png",
13 | "sizes": "36x36",
14 | "type": "image/png",
15 | "density": "0.75"
16 | },
17 | {
18 | "src": "icons/48-production.png",
19 | "sizes": "48x48",
20 | "type": "image/png",
21 | "density": "1.0"
22 | },
23 | {
24 | "src": "icons/72-production.png",
25 | "sizes": "72x72",
26 | "type": "image/png",
27 | "density": "1.5"
28 | },
29 | {
30 | "src": "icons/96-production.png",
31 | "sizes": "96x96",
32 | "type": "image/png",
33 | "density": "2.0"
34 | },
35 | {
36 | "src": "icons/144-production.png",
37 | "sizes": "144x144",
38 | "type": "image/png",
39 | "density": "3.0"
40 | },
41 | {
42 | "src": "icons/192-production.png",
43 | "sizes": "192x192",
44 | "type": "image/png",
45 | "density": "4.0"
46 | }
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/public/site.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/testing-library/testing-playground/2eae3e5c3d193e7792db3b9b00666cdc26bec929/public/site.jpg
--------------------------------------------------------------------------------
/scripts/build-extension.mts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx tsx
2 | import { argv, path, fs } from 'zx';
3 | import { build } from 'vite';
4 | import {config} from './vite.config.mjs';
5 | import { resolve as r, __DEV__ } from './utils.mjs'
6 | import webExt from 'web-ext';
7 |
8 | const TARGETS = ['chromium', 'firefox-desktop'];
9 | const { target } = argv;
10 |
11 | if (!TARGETS.includes(target)) {
12 | console.log(`
13 | Invalid target: ${target}, must be one of ${TARGETS.join(', ')}
14 |
15 | Usage:
16 | ./scripts/build-extension.mts --target chromium
17 | ./scripts/build-extension.mts --target firefox-desktop
18 | `);
19 | process.exit(1);
20 | }
21 |
22 | const root = r('devtools/src');
23 | const dest = r(`dist/${target}-extension`);
24 |
25 | const entries = [
26 | 'devtools/main.html',
27 | 'devtools/pane.html',
28 | 'devtools/panel.html',
29 | 'content-script/content-script.js',
30 | 'background/background.js',
31 | 'window/testing-library.js',
32 | ];
33 |
34 | await fs.rm(dest, { recursive: true, force: true });
35 | for (const entry of entries)
36 | {
37 | const isHTML = /\.html$/.test(entry);
38 | const dirname = path.dirname(entry);
39 | const basename = path.basename(entry);
40 |
41 | const libEntry = r(root, entry.replace(/\.html$/, '.js'));
42 | const entryFileNames = `${dirname}/${basename.replace(/\.html$/, '.js')}`;
43 |
44 | await new Promise((resolve) =>
45 | build({
46 | ...config,
47 | plugins: [
48 | ...config.plugins,
49 | { name: 'done', buildEnd: resolve },
50 | ],
51 | root,
52 | build: {
53 | watch: __DEV__ ? {} : null,
54 | sourcemap: __DEV__ ? 'inline' : false,
55 | outDir: r(`dist/${target}-extension`),
56 | emptyOutDir: false,
57 | minify: false,
58 | cssCodeSplit: false,
59 | lib: isHTML ? undefined : {
60 | entry: libEntry,
61 | fileName: (_, name) => `${dirname}/${name}.js`,
62 | formats: ['iife'],
63 | name: entry,
64 | },
65 | rollupOptions: isHTML ? {
66 | input: r(root, entry),
67 | output: {
68 | entryFileNames,
69 | manualChunks: () => null,
70 | preserveModules: false,
71 | }
72 | } : undefined,
73 | }
74 | })
75 | );
76 | }
77 |
78 | await fs.copy(r(root, 'manifest.json'), r(dest, 'manifest.json'));
79 | await fs.mkdir(r(dest, 'icons'), { recursive: true });
80 | const manifest = await fs.readJson(r(dest, 'manifest.json'));
81 |
82 | await Promise.all(
83 | Object.values(manifest.icons).map((icon: string) =>
84 | fs.copyFile(r('public', icon), r(dest, icon)),
85 | ),
86 | );
87 |
88 | if (__DEV__) {
89 | webExt.cmd.run({ sourceDir: dest, target, browserConsole: true, startUrl: 'https://meijer.ws' });
90 | } else {
91 | webExt.cmd.build({
92 | sourceDir: dest,
93 | target,
94 | artifactsDir: r('dist'),
95 | overwriteDest: true,
96 | filename: `${manifest.name}-${manifest.version}-${target}.zip`.replace(/\s/g, '-').toLowerCase(),
97 | });
98 | }
99 |
--------------------------------------------------------------------------------
/scripts/build-lambda.mts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx tsx
2 | import { $, fs } from 'zx';
3 | import { resolve as r } from "./utils.mjs";
4 |
5 | const root = r('src/lambda');
6 | const dest = r('dist/server');
7 |
8 | await fs.rm(dest, { recursive: true, force: true });
9 |
10 | if (!fs.existsSync(r('dist/public/index.html'))) {
11 | console.log(`public not built yet, building...`);
12 | await $`npm run build:public`;
13 | }
14 |
15 | await fs.copy(root, dest);
16 | await fs.copy(r('dist/public/index.html'), r(dest,'server/index.html'));
17 |
18 |
--------------------------------------------------------------------------------
/scripts/changelog.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | const conventionalChangelog = require('conventional-changelog');
3 | const spec = require('conventional-changelog-config-spec');
4 | const gitSemverTags = require('git-semver-tags');
5 | const { promisify } = require('util');
6 | const semverTags = promisify(gitSemverTags);
7 |
8 | function compact(obj) {
9 | const res = {};
10 |
11 | Object.keys(obj).forEach((key) => {
12 | if (obj[key] !== undefined) {
13 | res[key] = obj[key];
14 | }
15 | });
16 |
17 | return res;
18 | }
19 |
20 | const preset = {
21 | name: require.resolve('conventional-changelog-conventionalcommits'),
22 | types: spec.properties.types.default.map((x) => {
23 | if (x.type === 'fix') {
24 | return { ...x, section: 'Fixes' };
25 | }
26 |
27 | if (x.type === 'feat') {
28 | return x;
29 | }
30 |
31 | return { ...x, section: 'Other' };
32 | }),
33 | };
34 |
35 | async function generate({ version, from, to, showHeader } = {}) {
36 | let content = '';
37 |
38 | return new Promise((resolve, reject) => {
39 | const changelogStream = conventionalChangelog(
40 | {
41 | preset,
42 | tagPrefix: '',
43 | },
44 | { version },
45 | compact({ merges: null, path: './', from, to }),
46 | ).on('error', function (err) {
47 | return reject(err);
48 | });
49 |
50 | changelogStream.on('data', function (buffer) {
51 | content += buffer.toString();
52 | });
53 |
54 | changelogStream.on('end', function () {
55 | let lines = content.split('\n');
56 | if (showHeader) {
57 | lines[0] = `## Release Notes for ${lines[0].substr(3).trim()}`;
58 | } else {
59 | lines = lines.slice(3); // strip header and 2 blank lines
60 | }
61 |
62 | lines = lines.map((x) => (x.startsWith('#') ? x.substr(1) : x));
63 |
64 | content = lines.join('\n');
65 | return resolve(content);
66 | });
67 | });
68 | }
69 |
70 | async function main() {
71 | const [version, from] = await semverTags();
72 |
73 | generate({ version, from }).then((x) => {
74 | // eslint-disable-next-line no-console
75 | console.log(x);
76 | });
77 | }
78 |
79 | main();
80 |
--------------------------------------------------------------------------------
/scripts/crxmake.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Purpose: Pack a Chromium extension directory into crx format
4 | # src: https://stackoverflow.com/questions/18693962/pack-chrome-extension-on-server-with-only-command-line-interface
5 |
6 | if test $# -ne 2; then
7 | echo "Usage: crxmake.sh "
8 | exit 1
9 | fi
10 |
11 | dir=$1
12 | key=$2
13 | name=$(basename "$dir")
14 | crx="$name.crx"
15 | pub="$name.pub"
16 | sig="$name.sig"
17 | zip="$name.zip"
18 | tosign="$name.presig"
19 | binary_crx_id="$name.crxid"
20 | trap 'rm -f "$pub" "$sig" "$zip" "$tosign" "$binary_crx_id"' EXIT
21 |
22 |
23 | # zip up the crx dir
24 | cwd=$(pwd -P)
25 | (cd "$dir" && zip -qr -9 -X "$cwd/$zip" .)
26 |
27 |
28 | #extract crx id
29 | openssl rsa -in "$key" -pubout -outform der | openssl dgst -sha256 -binary -out "$binary_crx_id"
30 | truncate -s 16 "$binary_crx_id"
31 |
32 | #generate file to sign
33 | (
34 | # echo "$crmagic_hex $version_hex $header_length $pub_len_hex $sig_len_hex"
35 | printf "CRX3 SignedData"
36 | echo "00 12 00 00 00 0A 10" | xxd -r -p
37 | cat "$binary_crx_id" "$zip"
38 | ) > "$tosign"
39 |
40 | # signature
41 | openssl dgst -sha256 -binary -sign "$key" < "$tosign" > "$sig"
42 |
43 | # public key
44 | openssl rsa -pubout -outform DER < "$key" > "$pub" 2>/dev/null
45 |
46 |
47 | crmagic_hex="43 72 32 34" # Cr24
48 | version_hex="03 00 00 00" # 3
49 | header_length="45 02 00 00"
50 | header_chunk_1="12 AC 04 0A A6 02"
51 | header_chunk_2="12 80 02"
52 | header_chunk_3="82 F1 04 12 0A 10"
53 | (
54 | echo "$crmagic_hex $version_hex $header_length $header_chunk_1" | xxd -r -p
55 | cat "$pub"
56 | echo "$header_chunk_2" | xxd -r -p
57 | cat "$sig"
58 | echo "$header_chunk_3" | xxd -r -p
59 | cat "$binary_crx_id" "$zip"
60 | ) > "$crx"
61 | echo "Wrote $crx"
--------------------------------------------------------------------------------
/scripts/utils.mts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 | import * as path from "path";
3 |
4 | const __dirname = fileURLToPath(new URL('.', import.meta.url));
5 |
6 | export const repoRoot = path.resolve(__dirname, '..');
7 |
8 | export const __DEV__ = process.env.NODE_ENV === 'development';
9 |
10 | export const resolve = (...p) => {
11 | const segments = p.flatMap(p => p.replace(repoRoot, '').split('/'))
12 | return path.resolve(repoRoot, ...segments.filter(Boolean));
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/vite.config.mts:
--------------------------------------------------------------------------------
1 | import { InlineConfig } from 'vite';
2 | import { resolve as r } from './utils.mjs';
3 | import * as fs from 'fs-extra';
4 | import react from '@vitejs/plugin-react';
5 |
6 | export const config: InlineConfig = {
7 | envDir: r(),
8 | resolve: {
9 | alias: {
10 | '~': r(),
11 | events: 'rollup-plugin-node-polyfills/polyfills/events',
12 | },
13 | },
14 | esbuild: { loader: 'jsx', include: /src\/.*\.jsx?$/, exclude: [] },
15 | optimizeDeps: {
16 | esbuildOptions: {
17 | plugins: [
18 | {
19 | name: 'load-js-files-as-jsx',
20 | setup(build) {
21 | build.onLoad({ filter: /src\/.*\.js$/ }, async (args) => ({
22 | loader: 'jsx',
23 | contents: await fs.readFile(args.path, 'utf8'),
24 | }));
25 | },
26 | },
27 | ],
28 | },
29 | },
30 | plugins: [react()],
31 | };
32 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
3 | import { PreviewEventsProvider } from './context/PreviewEvents';
4 | import Loader from './components/Loader';
5 |
6 | const Playground = React.lazy(() => import('./pages/Playground'));
7 | const Embedded = React.lazy(() => import('./pages/Embedded'));
8 |
9 | function App() {
10 | return (
11 |
12 |
13 | }>
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/src/components/CopyButton.js:
--------------------------------------------------------------------------------
1 | /* global chrome */
2 | import React, { useState, useEffect } from 'react';
3 | import IconButton from './IconButton';
4 | import { CopyIcon, CheckIcon } from '@primer/octicons-react';
5 |
6 | const IS_DEVTOOL = !!(window.chrome && chrome.runtime && chrome.runtime.id);
7 |
8 | /**
9 | *
10 | * @param {string} suggestion
11 | */
12 | async function attemptCopyToClipboard(suggestion) {
13 | try {
14 | if (!IS_DEVTOOL && 'clipboard' in navigator) {
15 | await navigator.clipboard.writeText(suggestion);
16 | return true;
17 | }
18 |
19 | const input = Object.assign(document.createElement('input'), {
20 | type: 'text',
21 | value: suggestion,
22 | });
23 |
24 | document.body.append(input);
25 | input.select();
26 | document.execCommand('copy');
27 | input.remove();
28 |
29 | return true;
30 | } catch (error) {
31 | console.error(error);
32 | return false;
33 | }
34 | }
35 |
36 | /**
37 | *
38 | * @param {{
39 | * text: string | function;
40 | * title: string;
41 | * className: string;
42 | * variant: string;
43 | * }} props
44 | */
45 | function CopyButton({ text, title, className, variant }) {
46 | const [copied, setCopied] = useState(false);
47 |
48 | useEffect(() => {
49 | if (copied) {
50 | const timeout = setTimeout(() => {
51 | setCopied(false);
52 | }, 1500);
53 |
54 | return () => clearTimeout(timeout);
55 | }
56 | }, [copied]);
57 |
58 | async function handleClick() {
59 | let textToCopy = text;
60 | if (typeof text === 'function') {
61 | textToCopy = text();
62 | }
63 | const wasSuccessfullyCopied = await attemptCopyToClipboard(textToCopy);
64 |
65 | if (wasSuccessfullyCopied) {
66 | setCopied(true);
67 | }
68 | }
69 |
70 | return (
71 |
77 | {copied ? : }
78 |
79 | );
80 | }
81 |
82 | export default CopyButton;
83 |
--------------------------------------------------------------------------------
/src/components/CopyButton.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CopyButton from './CopyButton';
3 | import { render, fireEvent, act, waitFor } from '@testing-library/react';
4 |
5 | const defaultProps = {
6 | text: 'string',
7 | title: 'title',
8 | };
9 |
10 | beforeEach(() => {
11 | delete window.navigator.clipboard;
12 | delete document.execCommand;
13 | });
14 |
15 | it('renders without crashing given default props', () => {
16 | render();
17 | });
18 |
19 | it('attempts to copy to clipboard through navigator.clipboard', async () => {
20 | const clipboardSpy = vi.fn();
21 |
22 | window.navigator.clipboard = {
23 | writeText: clipboardSpy,
24 | };
25 |
26 | const { getByRole } = render();
27 |
28 | await act(async () => {
29 | fireEvent.click(getByRole('button'));
30 | });
31 |
32 | expect(clipboardSpy).toHaveBeenCalledWith(defaultProps.text);
33 | expect(clipboardSpy).toHaveBeenCalledTimes(1);
34 | });
35 |
36 | it('attempts to copy with legacy methods if navigator.clipboard is unavailable', async () => {
37 | const execCommandSpy = vi.fn();
38 |
39 | document.execCommand = execCommandSpy;
40 |
41 | const { getByRole } = render();
42 |
43 | await act(async () => {
44 | fireEvent.click(getByRole('button'));
45 | });
46 |
47 | expect(execCommandSpy).toHaveBeenCalledWith('copy');
48 | expect(execCommandSpy).toHaveBeenCalledTimes(1);
49 | });
50 |
51 | it('temporarily shows a different icon after copying', async () => {
52 | vi.useFakeTimers();
53 | const execCommandSpy = vi.fn();
54 |
55 | document.execCommand = execCommandSpy;
56 |
57 | const { getByRole } = render();
58 |
59 | const button = getByRole('button');
60 |
61 | const initialIcon = button.innerHTML;
62 |
63 | // act due to useEffect state change
64 | await act(async () => {
65 | fireEvent.click(button);
66 | });
67 |
68 | await waitFor(() => {
69 | expect(button.innerHTML).not.toBe(initialIcon);
70 | });
71 |
72 | // same here
73 | await act(async () => {
74 | vi.runAllTimers();
75 | });
76 |
77 | await waitFor(() => {
78 | expect(button.innerHTML).toBe(initialIcon);
79 | });
80 | });
81 |
82 | it('should accept funcition to get text to copy', async () => {
83 | const execCommandSpy = vi.fn();
84 | const getTextToCopy = () => 'copy';
85 |
86 | document.execCommand = execCommandSpy;
87 |
88 | const { getByRole } = render(
89 | ,
90 | );
91 |
92 | await act(async () => {
93 | fireEvent.click(getByRole('button'));
94 | });
95 |
96 | expect(execCommandSpy).toHaveBeenCalledWith('copy');
97 | expect(execCommandSpy).toHaveBeenCalledTimes(1);
98 | });
99 |
--------------------------------------------------------------------------------
/src/components/DomEvents.js:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { ChevronUpIcon, ChevronDownIcon } from '@primer/octicons-react';
3 |
4 | import AutoSizer from 'react-virtualized-auto-sizer';
5 | import { TrashcanIcon } from '@primer/octicons-react';
6 |
7 | import { VirtualScrollable } from './Scrollable';
8 | import IconButton from './IconButton';
9 | import CopyButton from './CopyButton';
10 | import EmptyStreetImg from '../images/EmptyStreetImg';
11 | import StickyList from './StickyList';
12 | import {
13 | usePreviewEvents,
14 | usePreviewEventsActions,
15 | } from '../context/PreviewEvents';
16 |
17 | function EventRecord({ index, style, data }) {
18 | const { id, type, name, element, selector } = data[index];
19 |
20 | return (
21 |
27 |
{id}
28 |
29 |
{type}
30 |
{name}
31 |
32 |
{element}
33 |
{selector}
34 |
35 | );
36 | }
37 |
38 | function DomEvents() {
39 | const listRef = useRef();
40 | const { sortDirection, buffer, appendMode, eventCount } = usePreviewEvents();
41 | const { changeSortDirection, reset } = usePreviewEventsActions();
42 |
43 | const getSortIcon = () => (
44 |
45 | {sortDirection.current === 'desc' ? (
46 |
47 | ) : (
48 |
49 | )}
50 |
51 | );
52 |
53 | const getTextToCopy = () =>
54 | buffer.current
55 | .map((log) => `${log.target.toString()} - ${log.event.EventType}`)
56 | .join('\n');
57 |
58 | return (
59 |
60 |
61 |
62 |
66 | # {getSortIcon()}
67 |
68 |
69 |
type
70 |
name
71 |
72 |
element
73 |
74 |
selector
75 |
76 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | {buffer.current.length === 0 ? (
90 |
91 |
92 |
93 | ) : (
94 |
95 | {({ width, height }) => (
96 |
106 | {EventRecord}
107 |
108 | )}
109 |
110 | )}
111 |
112 |
113 |
114 | );
115 | }
116 |
117 | export default DomEvents;
118 |
--------------------------------------------------------------------------------
/src/components/Embed.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import Input from './Input';
3 | import CopyButton from './CopyButton';
4 | import Embedded from '../pages/Embedded';
5 | import { SyncIcon, XIcon } from '@primer/octicons-react';
6 |
7 | import { defaultPanes } from '../constants';
8 | import TabButton from './TabButton';
9 |
10 | const possiblePanes = ['markup', 'preview', 'query', 'result'];
11 |
12 | const styles = {
13 | section: { width: 850 },
14 | frame: { width: 850, height: 375 },
15 | };
16 |
17 | // TODO: make the preview frame height match the end result, and let
18 | // the user modify the frame height
19 | function Embed({ dispatch, dirty, gistId, gistVersion }) {
20 | useEffect(() => {
21 | if (!dirty) {
22 | return;
23 | }
24 |
25 | dispatch({ type: 'SAVE' });
26 | }, [dirty, gistId, dispatch]);
27 |
28 | const [panes, setPanes] = useState(defaultPanes);
29 |
30 | const embedUrl =
31 | [location.origin, 'embed', gistId, gistVersion].filter(Boolean).join('/') +
32 | `?panes=${panes.join(',')}`;
33 |
34 | const embedCode = ``;
35 | const canAddPane = panes.length < 3;
36 |
37 | const loader = (
38 |
39 |
40 | one sec...
41 |
42 | );
43 |
44 | return (
45 |
46 |
47 |
48 |
49 |
Configure
50 |
52 | setPanes([
53 | ...panes,
54 | possiblePanes.find((x) => !panes.includes(x)),
55 | ])
56 | }
57 | disabled={!canAddPane}
58 | >
59 | add pane
60 |
61 |
62 |
63 | {/* overflow-hidden is required hide the hidden preview panel */}
64 |
65 |
66 | {panes.map((selected, idx) => (
67 |
68 |
69 | {possiblePanes.map((name) => (
70 |
73 | setPanes((current) => {
74 | const next = [...current];
75 | next[idx] = name;
76 | return next;
77 | })
78 | }
79 | active={selected === name}
80 | >
81 | {name}
82 |
83 | ))}
84 |
85 |
setPanes(panes.filter((_, i) => i !== idx))}
88 | >
89 |
90 |
91 |
92 |
93 |
94 | ))}
95 |
96 |
97 |
98 | {dirty ? null : (
99 |
105 | )}
106 |
107 |
108 |
109 |
110 |
144 |
145 |
146 | );
147 | }
148 |
149 | export default Embed;
150 |
--------------------------------------------------------------------------------
/src/components/EmptyPane.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import icon from '~/public/code_thinking.png';
3 |
4 | function EmptyPane() {
5 | return (
6 |
7 |
8 |

12 |
13 |
14 | );
15 | }
16 |
17 | export default EmptyPane;
18 |
--------------------------------------------------------------------------------
/src/components/ErrorBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Scrollable from './Scrollable';
3 |
4 | function ErrorBox({ caption, body }) {
5 | return (
6 |
7 |
8 |
9 |
Error: {caption}
10 |
11 |
12 | {body}
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
20 | export default ErrorBox;
21 |
--------------------------------------------------------------------------------
/src/components/Expandable.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import IconButton from './IconButton';
3 | import Scrollable from './Scrollable';
4 | import { ChevronUpIcon, ChevronDownIcon } from '@primer/octicons-react';
5 |
6 | function Expandable({ excerpt, children, className, variant, labelText }) {
7 | const [expanded, setExpanded] = useState(false);
8 |
9 | return (
10 |
20 | {expanded && (
21 |
22 |
23 |
26 |
27 |
28 |
setExpanded(!expanded)}
32 | >
33 |
34 |
35 |
36 | )}
37 |
38 | {expanded || !children ? (
39 |
40 | ) : (
41 |
42 | {excerpt || children}
43 |
44 | )}
45 |
46 | setExpanded(!expanded)}
50 | title="expand"
51 | >
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | export default Expandable;
59 |
--------------------------------------------------------------------------------
/src/components/IconButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const variants = {
4 | dark: 'text-gray-500 hover:text-gray-400',
5 | light: 'text-gray-500 hover:text-gray-700',
6 | white: 'text-white hover:text-white',
7 | };
8 |
9 | function IconButton({ children, title, variant, onClick, className }) {
10 | const cssVariant = variants[variant] || variants['light'];
11 | return (
12 |
25 | );
26 | }
27 |
28 | export default IconButton;
29 |
--------------------------------------------------------------------------------
/src/components/Input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Input(props) {
4 | return (
5 |
9 | );
10 | }
11 |
12 | export default Input;
13 |
--------------------------------------------------------------------------------
/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Header from './Header';
3 | import { ToastContainer } from 'react-toastify';
4 |
5 | function Layout({
6 | children,
7 | dirty,
8 | gistId,
9 | gistVersion,
10 | dispatch,
11 | status,
12 | settings,
13 | }) {
14 | return (
15 |
16 |
17 |
27 |
28 |
29 | {/*not sure why, but safari needs a height here*/}
30 |
31 | {children}
32 |
33 |
34 |
41 |
42 | );
43 | }
44 |
45 | export default Layout;
46 |
--------------------------------------------------------------------------------
/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import frog from '~/public/icons/128-production.png';
3 |
4 | function Loader({ loading }) {
5 | return (
6 |
12 |

13 |
14 | );
15 | }
16 |
17 | export default Loader;
18 |
--------------------------------------------------------------------------------
/src/components/MarkupEditor.js:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useState } from 'react';
2 | import Editor from './Editor';
3 |
4 | function MarkupEditor({ markup, dispatch }) {
5 | const [initialValue] = useState(markup);
6 |
7 | const onLoad = useCallback(
8 | (editor) => dispatch({ type: 'SET_MARKUP_EDITOR', editor }),
9 | [dispatch],
10 | );
11 |
12 | const onChange = useCallback(
13 | (markup, { origin }) =>
14 | dispatch({
15 | type: 'SET_MARKUP',
16 | markup,
17 | origin: 'EDITOR',
18 | immediate: origin === 'user',
19 | }),
20 | [dispatch],
21 | );
22 |
23 | return (
24 |
34 | );
35 | }
36 |
37 | export default React.memo(MarkupEditor);
38 |
--------------------------------------------------------------------------------
/src/components/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as ReachMenu from '@reach/menu-button';
3 |
4 | export const Menu = ReachMenu.Menu;
5 |
6 | export const MenuButton = (props) => (
7 |
8 | );
9 |
10 | export const MenuPopover = (props) => (
11 |
15 | );
16 |
17 | export const MenuList = (props) => (
18 |
22 | );
23 |
24 | export const MenuLink = (props) => (
25 |
30 | );
31 |
--------------------------------------------------------------------------------
/src/components/Modal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Dialog } from '@reach/dialog';
3 | import { XIcon } from '@primer/octicons-react';
4 | import IconButton from './IconButton';
5 |
6 | const callAll = (...fns) => (...args) => fns.forEach((fn) => fn && fn(...args));
7 |
8 | const ModalContext = React.createContext();
9 |
10 | function Modal(props) {
11 | const [isOpen, setIsOpen] = React.useState(props.open ?? false);
12 |
13 | return ;
14 | }
15 |
16 | function ModalDismissButton({ children: child }) {
17 | const [, setIsOpen] = React.useContext(ModalContext);
18 | return React.cloneElement(child, {
19 | onClick: callAll(() => setIsOpen(false), child.props.onClick),
20 | });
21 | }
22 |
23 | function ModalOpenButton({ children: child }) {
24 | const [, setIsOpen] = React.useContext(ModalContext);
25 | return React.cloneElement(child, {
26 | onClick: callAll(() => setIsOpen(true), child.props.onClick),
27 | });
28 | }
29 |
30 | function ModalContentsBase(props) {
31 | const [isOpen, setIsOpen] = React.useContext(ModalContext);
32 | return (
33 |