├── .distignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── checks.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .wordpress-org ├── banner-1544x500.png ├── banner-772x250.png ├── banner.psd ├── icon-128x128.png ├── icon-256x256.png ├── icon.svg ├── screenshot-1.gif ├── screenshot-2.gif ├── screenshot-3.jpg ├── screenshot-4.jpg └── screenshot-5.jpg ├── README.md ├── README.txt ├── ab-testing-for-wp.php ├── babel.config.js ├── blog ├── .gitignore ├── README.md ├── _config.production.yml ├── _config.yml ├── package-lock.json ├── package.json ├── scaffolds │ ├── draft.md │ ├── page.md │ └── post.md ├── source │ ├── 404.md │ ├── _posts │ │ ├── add-tests-anywhere-on-your-site.md │ │ ├── add-tests-anywhere-on-your-site │ │ │ ├── screenshot-1.png │ │ │ └── screenshot-2.png │ │ ├── convert-blocks-to-tests.md │ │ ├── convert-blocks-to-tests │ │ │ └── convert-to-test.png │ │ ├── features-so-far-planning-ahead.md │ │ ├── features-so-far-planning-ahead │ │ │ ├── example.jpg │ │ │ ├── screenshot-1.jpg │ │ │ └── screenshot-2.jpg │ │ ├── form-integrations.md │ │ ├── form-integrations │ │ │ └── html-forms-integration.jpg │ │ ├── new-form-integrations.md │ │ ├── place-visitors-variant-using-url-parameters.md │ │ ├── place-visitors-variant-using-url-parameters │ │ │ └── ab-test-conditions.png │ │ ├── track-outbound-links.md │ │ └── track-outbound-links │ │ │ └── screenshot.png │ └── favicon.ico └── themes │ └── ab-testing-for-wp │ ├── _config.yml │ ├── layout │ ├── archive.ejs │ ├── index.ejs │ ├── layout.ejs │ ├── page.ejs │ ├── partials │ │ ├── article.ejs │ │ ├── footer.ejs │ │ ├── head.ejs │ │ ├── header.ejs │ │ ├── page.ejs │ │ └── post │ │ │ ├── date.ejs │ │ │ └── title.ejs │ └── post.ejs │ └── source │ ├── css │ ├── _article.scss │ ├── _footer.scss │ ├── _frontpage.scss │ ├── _header.scss │ ├── _reset.scss │ ├── _variables.scss │ └── style.scss │ ├── images │ ├── campaign-tweaking.svg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── financial-analysis.svg │ ├── icon-light.svg │ ├── icon.svg │ ├── integrations.jpg │ ├── market-research.svg │ ├── monitor.svg │ ├── og_abtestingforwp_poster.jpg │ ├── screenshot-1.gif │ ├── screenshot-2.gif │ ├── screenshot-3.jpg │ ├── seo-report.svg │ ├── web-development.svg │ └── web-protection.svg │ └── integrations.psd ├── composer.json ├── cypress.json ├── cypress ├── data │ ├── wordpress_e2e_2020-01-17.sql │ └── wordpress_e2e_2020-04-06.sql ├── integration │ ├── ab-testing.spec.ts │ ├── admin-bar.spec.ts │ ├── how-to.spec.ts │ ├── onboarding.spec.ts │ ├── outbound.spec.ts │ ├── overview.spec.ts │ ├── plugin-activation.spec.ts │ ├── stand-alone.spec.ts │ └── test-results.spec.ts ├── plugins │ └── index.js ├── support │ ├── commands.ts │ ├── index.ts │ └── patches.ts └── types │ └── index.d.ts ├── docker-compose-e2e.yml ├── docker-compose.yml ├── package-lock.json ├── package.json ├── scripts ├── bump.sh └── release.sh ├── src ├── assets │ ├── ab-testing-for-wp-base64-logo.svg │ ├── ab-testing-for-wp-logo-icon.svg │ ├── ab-testing-for-wp-logo-square.eps │ ├── ab-testing-for-wp-logo.svg │ ├── how-to-1.png │ ├── how-to-2.png │ ├── how-to-3.png │ ├── how-to-4.png │ └── plugin-gutenberg-demo.gif ├── css │ ├── admin-bar.css │ └── admin.css ├── js │ ├── admin-bar.tsx │ ├── admin-editor.ts │ ├── admin-page.tsx │ ├── blocks │ │ ├── ab-test-inserter.tsx │ │ ├── ab-test-variant.tsx │ │ └── ab-test.tsx │ ├── components │ │ ├── Admin │ │ │ ├── Admin.tsx │ │ │ ├── components │ │ │ │ └── Table │ │ │ │ │ └── Table.tsx │ │ │ └── pages │ │ │ │ └── Overview │ │ │ │ ├── Overview.css │ │ │ │ └── Overview.tsx │ │ ├── AdminBar │ │ │ ├── AdminBar.tsx │ │ │ ├── Test.tsx │ │ │ ├── Variant.tsx │ │ │ └── helpers │ │ │ │ └── highlight.ts │ │ ├── BoxShadow │ │ │ ├── BoxShadow.css │ │ │ └── BoxShadow.tsx │ │ ├── GeneralSettings │ │ │ ├── GeneralSettings.css │ │ │ └── GeneralSettings.tsx │ │ ├── GoalSelector │ │ │ └── GoalSelector.tsx │ │ ├── Inserter │ │ │ ├── Inserter.css │ │ │ └── Inserter.tsx │ │ ├── Loader │ │ │ └── Loader.tsx │ │ ├── Logo │ │ │ └── Logo.tsx │ │ ├── Onboarding │ │ │ ├── Arrow.tsx │ │ │ ├── Onboarding.css │ │ │ ├── Onboarding.tsx │ │ │ └── Overlay.tsx │ │ ├── Significance │ │ │ ├── Significance.css │ │ │ └── Significance.tsx │ │ ├── TestPreview │ │ │ ├── EditWrapper.css │ │ │ ├── EditWrapper.tsx │ │ │ └── TestPreview.tsx │ │ ├── TestResults │ │ │ ├── DeclareWinner.tsx │ │ │ ├── TestResults.css │ │ │ └── TestResults.tsx │ │ ├── VariantSelector │ │ │ ├── VariantSelector.css │ │ │ └── VariantSelector.tsx │ │ └── VariantSettings │ │ │ ├── Conditionals.css │ │ │ ├── Conditionals.tsx │ │ │ ├── ControlSettings.tsx │ │ │ ├── DistributionSettings.tsx │ │ │ ├── VariantSettings.css │ │ │ └── VariantSettings.tsx │ ├── core │ │ └── allowedBlockTypes.ts │ ├── frontend.ts │ ├── frontend │ │ ├── doNotTrack.ts │ │ ├── handleTestRender.ts │ │ └── handleTestTracking.ts │ ├── helpers │ │ ├── calcTestWinner.ts │ │ ├── options.ts │ │ └── wordpress.ts │ ├── plugins │ │ └── ConvertButton │ │ │ └── ConvertButton.tsx │ └── types │ │ └── ab-testing-for-wp.d.ts └── php │ ├── actions │ ├── goals.php │ ├── options.php │ ├── posts.php │ └── tests.php │ ├── data │ ├── ab-test-manager.php │ ├── ab-test-stats.php │ ├── ab-test-tracking.php │ ├── installer.php │ └── options-manager.php │ ├── helpers │ ├── ab-test-content-parser.php │ ├── block-renderer.php │ ├── cookie-manager.php │ └── do-not-track.php │ ├── integrations │ ├── Integration.php │ ├── bootstrap.php │ ├── contact-form-7 │ │ └── ContactForm7.php │ ├── formidable │ │ └── Formidable.php │ ├── gravityforms │ │ └── GravityForms.php │ ├── html-forms │ │ └── HTMLForms.php │ ├── mc4wp │ │ └── MC4WP.php │ ├── ninja-forms │ │ └── NinjaForms.php │ └── wpforms │ │ └── WPForms.php │ ├── pages │ ├── advanced.php │ └── howto.php │ └── registrations │ ├── register-admin-page.php │ ├── register-custom-post-type.php │ ├── register-frontend-admin-bar.php │ ├── register-gutenberg-blocks.php │ ├── register-render-scripts.php │ ├── register-rest.php │ └── register-shortcode.php ├── tsconfig.json └── webpack.config.js /.distignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.github 3 | /.idea 4 | /.wordpress-org 5 | /cypress 6 | /node_modules 7 | /blog 8 | /wp-content 9 | /wp-content-e2e 10 | *.DS_Store 11 | ab-testing-for-wp.zip 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | indent_size = 4 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.html] 18 | indent_size = 4 19 | 20 | [*.php] 21 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | wp-content/ 4 | wp-content-e2e/ 5 | cypress/**/*.js 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'airbnb', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:cypress/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | plugins: ['@typescript-eslint', 'cypress', 'react-hooks'], 10 | env: { 11 | browser: true, 12 | node: true, 13 | 'cypress/globals': true, 14 | }, 15 | globals: { 16 | wp: false, 17 | ABTestingForWP: false, 18 | ABTestingForWP_AdminBar: false, 19 | ABTestingForWP_Options: false, 20 | ABTestingForWP_Data: false, 21 | }, 22 | rules: { 23 | '@typescript-eslint/camelcase': 0, 24 | 'cypress/no-unnecessary-waiting': 0, 25 | 'react/jsx-props-no-spreading': 0, 26 | 'react/prop-types': 0, 27 | 'react/require-default-props': 0, 28 | 'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }], 29 | 'import/extensions': [ 30 | 'error', 31 | 'ignorePackages', 32 | { 33 | js: 'never', 34 | jsx: 'never', 35 | ts: 'never', 36 | tsx: 'never', 37 | } 38 | ], 39 | }, 40 | settings: { 41 | 'import/resolver': { 42 | node: { 43 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 44 | }, 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check: 7 | name: Check source 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v1 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | - name: Install 15 | run: npm ci 16 | - name: Lint 17 | run: npm run lint 18 | - name: Check TypeScript 19 | run: npm run tsc 20 | - name: Build 21 | run: npm run build 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | name: Release plugin 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@master 15 | - name: Use Node.js 16 | uses: actions/setup-node@v1 17 | - name: Use Composer 18 | uses: MilesChou/composer-action/5.6/install@master 19 | with: 20 | args: dumpautoload 21 | - name: Build project 22 | run: | 23 | npm ci 24 | npm run clean-build 25 | - name: WordPress Plugin Deploy 26 | uses: 10up/action-wordpress-plugin-deploy@master 27 | env: 28 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 29 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 30 | SLUG: ab-testing-for-wp 31 | - name: Archive project 32 | run: npm run archive 33 | - name: Create Release on GitHub 34 | id: create_release 35 | uses: actions/create-release@v1.0.0 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | with: 39 | tag_name: ${{ github.ref }} 40 | release_name: Release ${{ github.ref }} 41 | draft: false 42 | prerelease: false 43 | - name: Upload Release Asset to GitHub 44 | id: upload-release-asset 45 | uses: actions/upload-release-asset@v1.0.1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} 50 | asset_path: ./ab-testing-for-wp.zip 51 | asset_name: ab-testing-for-wp.zip 52 | asset_content_type: application/zip 53 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | name: E2E tests 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@master 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | - name: Use Composer 15 | uses: MilesChou/composer-action/5.6/install@master 16 | with: 17 | args: dumpautoload 18 | - name: Install 19 | run: npm ci 20 | - name: Build 21 | run: npm run clean-build 22 | - name: Setup environments 23 | run: npm run e2e:setup-env 24 | - name: Test 25 | run: npm run e2e:run -- --record=${{ secrets.CYPRESS_RECORD }} 26 | env: 27 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cypress/videos/ 2 | cypress/screenshots/ 3 | dist/ 4 | node_modules/ 5 | vendor/ 6 | wp-content/ 7 | wp-content-e2e/ 8 | ab-testing-for-wp.zip 9 | stats.json 10 | -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/banner-1544x500.png -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/banner-772x250.png -------------------------------------------------------------------------------- /.wordpress-org/banner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/banner.psd -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/icon-128x128.png -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/icon-256x256.png -------------------------------------------------------------------------------- /.wordpress-org/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | -------------------------------------------------------------------------------- /.wordpress-org/screenshot-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/screenshot-1.gif -------------------------------------------------------------------------------- /.wordpress-org/screenshot-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/screenshot-2.gif -------------------------------------------------------------------------------- /.wordpress-org/screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/screenshot-3.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/screenshot-4.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/.wordpress-org/screenshot-5.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A/B Testing for WordPress 2 | 3 | WordPress plugin which allow you to run A/B tests from anywhere within your content. 4 | 5 | Utilize the new Gutenberg editor to create split tests to serve to your visitors and find out 6 | which variation works best. 7 | 8 | ![Gutenberg Demo](src/assets/plugin-gutenberg-demo.gif) 9 | 10 | ## Installing 11 | 12 | 1. Download `ab-testing-for-wp.zip` found on the [latest release](https://github.com/Gaya/ab-testing-for-wp/releases/latest) 13 | 1. Unzip contents and upload "ab-testing-for-wp" folder to the "/wp-content/plugins/" directory. 14 | 1. Activate the plugin through the "Plugins" screen in WordPress. 15 | 1. You can now add tests to your content! 16 | 17 | ## Requirements 18 | 19 | - At least WordPress 5.0 (uses the new Gutenberg editor) 20 | 21 | ## JavaScript Bundle Development 22 | 23 | Requirements: [Node.js](https://nodejs.org/en/) 24 | 25 | Clone the project and `npm install`. 26 | 27 | Use the following commands: 28 | 29 | ``` 30 | # one time build 31 | npm run build 32 | 33 | # development watch mode for JavaScript 34 | npm run dev 35 | 36 | # prepare for release (clean, build, archive) 37 | npm run release 38 | ``` 39 | 40 | ## Using Docker for development 41 | 42 | Requirements: [Composer](https://getcomposer.org/), [Docker](https://www.docker.com/products/developer-tools) 43 | 44 | ``` 45 | # Starting Docker container: 46 | docker-compose up -d 47 | ``` 48 | 49 | Development WordPress install now runs at [localhost:8000](http://localhost:8000) 50 | 51 | `./wp-content` of the project root is synced with the development install's `wp-content`. 52 | 53 | Look at `docker-compose.yml` for database passwords. 54 | 55 | ## Testing and linting 56 | 57 | This project is tested using [Cypress](https://www.cypress.io/) and linted using [eslint](https://eslint.org/). These dependencies get installed with the project automatically. 58 | 59 | ### ESLint 60 | 61 | Linting makes sure the code style is in order. This will also be performed on the main repository when creating a pull request. 62 | 63 | In order to run linting on your local environment run the following command from the root of the project: 64 | 65 | ``` 66 | npm run lint 67 | ``` 68 | 69 | ### Cypress 70 | 71 | Running all end-to-end tests makes sure all functionality is still in place after updates to the code have been made. 72 | 73 | Run all tests by entering the following command: 74 | 75 | ``` 76 | npm run test 77 | ``` 78 | 79 | There are a few extra command to help you test and develop locally: 80 | 81 | ``` 82 | # run local test environment and open Cypress 83 | npm run test:dev 84 | 85 | # tear down the setup environment and create a new one 86 | npmr run e2e:setup-env 87 | 88 | # reset the database to be a fresh WordPress install 89 | npm run e2e:reset-db 90 | ``` 91 | 92 | 93 | -------------------------------------------------------------------------------- /ab-testing-for-wp.php: -------------------------------------------------------------------------------- 1 | . 26 | */ 27 | 28 | namespace ABTestingForWP; 29 | 30 | if (!defined('ABSPATH')) { 31 | header('Status: 403 Forbidden'); 32 | header('HTTP/1.1 403 Forbidden'); 33 | exit; 34 | } 35 | 36 | require __DIR__ . '/vendor/autoload.php'; 37 | 38 | function bootstrap() { 39 | // on every request 40 | new RegisterGutenbergBlocks(__FILE__); 41 | new RegisterCustomPostType(); 42 | new RegisterShortcode(); 43 | new BootStrapIntegrations(); 44 | 45 | // only on admin 46 | if(is_admin()) { 47 | if(!defined('DOING_AJAX') || !DOING_AJAX) { 48 | new RegisterAdminPage(__FILE__); 49 | } 50 | } 51 | 52 | // only on frontend 53 | if(!is_admin()) { 54 | new RegisterRenderScripts(__FILE__); 55 | new RegisterFrontendAdminBar(__FILE__); 56 | } 57 | } 58 | 59 | function bootstrapREST() { 60 | new RegisterREST(); 61 | } 62 | 63 | // register WordPress hooks 64 | new Installer(__FILE__); 65 | bootstrap(); 66 | add_action('rest_api_init', 'ABTestingForWP\\bootstrapREST'); 67 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | [ 8 | '@babel/plugin-transform-react-jsx', 9 | { 10 | pragma: 'wp.element.createElement', 11 | pragmaFrag: 'wp.element.Fragment', 12 | }, 13 | ], 14 | '@babel/plugin-proposal-class-properties', 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /blog/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | db.json 4 | *.log 5 | node_modules/ 6 | public/ 7 | .deploy*/ 8 | _multiconfig.yml 9 | -------------------------------------------------------------------------------- /blog/README.md: -------------------------------------------------------------------------------- 1 | # A/B Testing for WordPress blog 2 | 3 | Source of the [A/B Testing for WordPress](https://abtestingforwp.com/blog/) website and blog. 4 | 5 | Uses [Hexo](https://hexo.io) to generate site and blog. 6 | 7 | ## Useful commands 8 | 9 | ```bash 10 | # develop 11 | npm run serve 12 | 13 | # build 14 | npm run build 15 | ``` 16 | -------------------------------------------------------------------------------- /blog/_config.production.yml: -------------------------------------------------------------------------------- 1 | # Hexo Configuration 2 | 3 | # URL 4 | url: https://abtestingforwp.com 5 | root: __ROOT_URL__/ 6 | -------------------------------------------------------------------------------- /blog/_config.yml: -------------------------------------------------------------------------------- 1 | # Hexo Configuration 2 | ## Docs: https://hexo.io/docs/configuration.html 3 | ## Source: https://github.com/hexojs/hexo/ 4 | 5 | # Site 6 | title: A/B Testing for WordPress 7 | subtitle: Easiest way to create split tests on your WordPress sites, right from the content editor! 8 | description: WordPress plugin to create A/B and split tests right from your content editor 9 | keywords: A/B testing, marketing, split test, optimise, measure, WordPress 10 | author: CleverNode 11 | language: en-UK 12 | timezone: CET 13 | 14 | # URL 15 | ## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/' 16 | url: http://localhost:4000/ 17 | root: / 18 | permalink: blog/:title/ 19 | permalink_defaults: 20 | 21 | # Directory 22 | source_dir: source 23 | public_dir: public 24 | tag_dir: tags 25 | archive_dir: blog 26 | category_dir: categories 27 | code_dir: downloads/code 28 | i18n_dir: :lang 29 | skip_render: 30 | 31 | # Writing 32 | new_post_name: :title.md # File name of new posts 33 | default_layout: post 34 | titlecase: false # Transform title into titlecase 35 | external_link: true # Open external links in new tab 36 | filename_case: 0 37 | render_drafts: false 38 | post_asset_folder: true 39 | relative_link: false 40 | future: true 41 | highlight: 42 | enable: true 43 | line_number: true 44 | auto_detect: false 45 | tab_replace: 46 | 47 | # Home page setting 48 | # path: Root path for your blogs index page. (default = '') 49 | # per_page: Posts displayed per page. (0 = disable pagination) 50 | # order_by: Posts order. (Order by date descending by default) 51 | index_generator: 52 | path: '' 53 | per_page: 10 54 | order_by: -date 55 | 56 | # Category & Tag 57 | default_category: uncategorized 58 | category_map: 59 | tag_map: 60 | 61 | # Date / Time format 62 | ## Hexo uses Moment.js to parse and display date 63 | ## You can customize the date format as defined in 64 | ## http://momentjs.com/docs/#/displaying/format/ 65 | date_format: MMMM Do, YYYY 66 | time_format: HH:mm:ss 67 | 68 | # Pagination 69 | ## Set per_page to 0 to disable pagination 70 | per_page: 10 71 | pagination_dir: page 72 | 73 | # Extensions 74 | ## Plugins: https://hexo.io/plugins/ 75 | ## Themes: https://hexo.io/themes/ 76 | theme: ab-testing-for-wp 77 | 78 | # Deployment 79 | ## Docs: https://hexo.io/docs/deployment.html 80 | deploy: 81 | type: 82 | -------------------------------------------------------------------------------- /blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-site", 3 | "version": "0.0.0", 4 | "private": true, 5 | "hexo": { 6 | "version": "4.2.1" 7 | }, 8 | "scripts": { 9 | "serve": "hexo server", 10 | "build": "npm run generate && npm run replace", 11 | "generate": "hexo generate --config _config.yml,_config.production.yml", 12 | "replace": "replace-in-file /__ROOT_URL__/g 'https://abtestingforwp.com' public/*.html,public/**/*.html,public/**/**/*.html --isRegex" 13 | }, 14 | "dependencies": { 15 | "hexo": "^4.2.0", 16 | "hexo-browsersync": "^0.3.0", 17 | "hexo-generator-archive": "^1.0.0", 18 | "hexo-generator-category": "^1.0.0", 19 | "hexo-generator-index": "^1.0.0", 20 | "hexo-generator-tag": "^1.0.0", 21 | "hexo-renderer-ejs": "^1.0.0", 22 | "hexo-renderer-marked": "^2.0.0", 23 | "hexo-renderer-sass": "^0.4.0", 24 | "hexo-renderer-stylus": "^1.1.0", 25 | "hexo-server": "^1.0.0", 26 | "replace-in-file": "^5.0.2" 27 | } 28 | } -------------------------------------------------------------------------------- /blog/scaffolds/draft.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ title }} 3 | tags: 4 | --- 5 | -------------------------------------------------------------------------------- /blog/scaffolds/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ title }} 3 | date: {{ date }} 4 | --- 5 | -------------------------------------------------------------------------------- /blog/scaffolds/post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: {{ title }} 3 | date: {{ date }} 4 | description: 5 | --- 6 | -------------------------------------------------------------------------------- /blog/source/404.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Whoops! Not found 3 | --- 4 | We're really sorry, but it looks like the link you wanted to visit is not available. 5 | 6 | [Return to the homepage](/) 7 | -------------------------------------------------------------------------------- /blog/source/_posts/add-tests-anywhere-on-your-site.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Add Tests Anywhere on Your Site 3 | date: 2019-04-19 16:00:00 4 | description: Create stand-alone A/B tests to add anywhere on your WordPress site 5 | --- 6 | In the latest version of [A/B Testing for WordPress](https://wordpress.org/plugins/ab-testing-for-wp/) it is possible to add new tests outside of the regular content editor way. 7 | 8 | {% asset_img screenshot-1.png Creating a stand-alone test %} 9 | 10 | ## Stand-alone Tests 11 | 12 | You can create a new stand-alone test from the admin sidebar or by choosing "Add New" on the tests overview page. 13 | 14 | These tests will be available through a shortcode, which can be used anywhere on your site. 15 | 16 | Make sure you "publish" the stand-alone test and toggle the "Run this test" setting in the general settings to make it available. 17 | 18 | ## Using in Your Theme / Template 19 | 20 | You can place the shortcode of the test anywhere in your theme's PHP code, this means you can for instance place your tests in the header or footer of your site. 21 | Not only in the content anymore! 22 | 23 | Place the following code anywhere in your theme: 24 | 25 |
<?php echo do_shortcode("[ab-test id=1234]"); ?>
26 | 27 | Replace "1234" with the number visible in the sidebar while editing a stand-alone test. You can also find this number in the A/B tests overview. 28 | 29 | ## Using Stand-alone Tests in Regular Content 30 | 31 | When you have stand-alone tests ready and published, you can also add them to your post and page content through the content editor. 32 | 33 | {% asset_img screenshot-2.png Inserting a stand-alone test %} 34 | 35 | Insert an A/B test like you would normally. It gives you the choice to create a new test in the content or insert an existing test. 36 | 37 | Pick the test you want to insert, confirm your choice, and a preview of your test will be visible in the content editor. 38 | 39 | To edit the stand-alone test: click on the "edit test" button which appears when hovering over the test. 40 | 41 | ## Tests as a Custom Post Type 42 | 43 | In technical terms, stand-alone tests are so called "custom post types" which can be interacted with by other WordPress developers and makes A/B Testing for WordPress a lot more customizable. 44 | -------------------------------------------------------------------------------- /blog/source/_posts/add-tests-anywhere-on-your-site/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/add-tests-anywhere-on-your-site/screenshot-1.png -------------------------------------------------------------------------------- /blog/source/_posts/add-tests-anywhere-on-your-site/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/add-tests-anywhere-on-your-site/screenshot-2.png -------------------------------------------------------------------------------- /blog/source/_posts/convert-blocks-to-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Convert Gutenberg Content Blocks to A/B Tests 3 | date: 2019-08-15 16:00:00 4 | description: How to convert your existing content blocks into A/B tests with A/B Testing for WordPress 5 | --- 6 | It is now possible to convert your existing content blocks into an A/B test. 7 | 8 | This way you can easily experiment with the content block you've already put on your pages! 9 | 10 | You do not have to create the same block two times to put inside of tests anymore, design your blocks upfront and convert them into a test to have duplications over two variants. 11 | 12 | Find this addition in the latest version of [A/B Testing for WordPress](https://wordpress.org/plugins/ab-testing-for-wp/). 13 | 14 | ## How to Convert Blocks Into Tests 15 | 16 | {% asset_img convert-to-test.png Convert a content block to a test %} 17 | 18 | 1. In the content editor, find the content block you want to convert into a test, and select it. 19 | 2. Press "more options" (1. in figure). 20 | 3. Choose "Convert to A/B test" (2. in figure). 21 | 22 | This will convert the block into an A/B test placing a copy of the block in both the A and B variants. 23 | 24 | Do not forget to enable the test to run it. 25 | 26 | It is not possible to convert blocks inside of an existing A/B Test to an A/B test. 27 | -------------------------------------------------------------------------------- /blog/source/_posts/convert-blocks-to-tests/convert-to-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/convert-blocks-to-tests/convert-to-test.png -------------------------------------------------------------------------------- /blog/source/_posts/features-so-far-planning-ahead.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Features so Far, and Planning Ahead 3 | date: 2019-04-02 4 | description: A/B Testing for WordPress features during the first weeks, and what's ahead for the plugin. 5 | --- 6 | 7 | About two weeks ago the first version of [A/B Testing for WordPress](/) was released. 8 | 9 | The response so far has been really good and I am looking forward to bringing the plugin to a higher level quickly. 10 | 11 | The plugin is [available for download on WordPress.org](https://wordpress.org/plugins/ab-testing-for-wp/) and can be found in the plugins directory. 12 | 13 | ## Features so Far 14 | 15 | Because the plugin is in a very early stage, the feature set might look a bit bare bones, but it's already pretty powerful! 16 | 17 | ### Create A/B (split) Tests in the Content Editor 18 | 19 | The main feature of A/B Testing for WordPress right now is the ability to create tests right in the content editor of WordPress. 20 | 21 | You can add a test, define the content of the variants in the test (A and B), and adjust the options, all in one place. 22 | 23 | {% asset_img screenshot-1.jpg Creating tests in the content editor %} 24 | 25 | ### Safe for SEO 26 | 27 | The standard, or control, variant of your test will always show to search engines and caching layers. This way the original content of your site will always stay the same for search engine and crawler bots. 28 | 29 | ### Measure the Results 30 | 31 | When visitors are presented with one of the variants they will partake in the test, once they visit (convert) the goal you want to measure they will end up in the "conversions" count. 32 | 33 | {% asset_img screenshot-2.jpg Test results %} 34 | 35 | You can see the winning variant as it has the highest percentage of conversions. 36 | 37 | At the moment you can setup posts and pages as goals for a test. 38 | 39 | ### Preview Variants on Your Site 40 | 41 | You can preview the variants of a test on your website by toggling between variants in the "admin bar". 42 | 43 | WordPress adds a bar at the top of your website for easy access to admin functionality. Here you can switch between variants of a test to see the result it produces. 44 | 45 | ### Declaring a Winner 46 | 47 | When you feel like the test has had enough time and there is a clear winner, you can declare a winner by going to the test options in the content editor. 48 | 49 | Pick a winner, and the contents of the variant will be "broken free" of the test. The test will also be removed from the page to make room for the winning variant. 50 | 51 | ## Planning Ahead 52 | 53 | There is a lot I want to include in the plugin, and that will take time to implement and develop. For now there is a rough plan for features which are most likely to be implemented first. 54 | 55 | ### Split Testing Pages 56 | 57 | When you do a split test, you might also want to test the complete content of a page. So you will be able to serve different landing pages to groups of people for instance. 58 | 59 | ### Integrate with E-commerce 60 | 61 | It would be valuable to be able to track what helps converting people to buy your products. 62 | 63 | Think about optimising the sales and decrease cart abandonment through performing tests. 64 | 65 | ### Variations based on segments 66 | 67 | A feature which has been requested a few times is to have variants based on segments. Meaning you can show different variations of a test to people visiting from a certain location or using a certain device. 68 | 69 | ### More variants than A and B 70 | 71 | Right now, you can add A and B variants, but have more than two different variants would be cool too! 72 | 73 | ## Get in Touch 74 | 75 | If you have any questions, or if you have suggestions about features you miss / would like to see improved, do not hesitate to [contact me through info@abtestingforwp.com](mailto:info@abtestingforwp.com). 76 | -------------------------------------------------------------------------------- /blog/source/_posts/features-so-far-planning-ahead/example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/features-so-far-planning-ahead/example.jpg -------------------------------------------------------------------------------- /blog/source/_posts/features-so-far-planning-ahead/screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/features-so-far-planning-ahead/screenshot-1.jpg -------------------------------------------------------------------------------- /blog/source/_posts/features-so-far-planning-ahead/screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/features-so-far-planning-ahead/screenshot-2.jpg -------------------------------------------------------------------------------- /blog/source/_posts/form-integrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Form Plugin Integrations 3 | date: 2019-04-04 10:59:38 4 | description: Use form submits and sign ups as a goal to track for A/B Testing for WordPress 5 | --- 6 | [The latest version (1.6.0) of A/B Testing for WordPress](https://wordpress.org/plugins/ab-testing-for-wp/) now supports choosing form actions as a goal to track! 7 | 8 | Keep track of form submissions and see which variant of your content converts the most visitors to interact with your forms. 9 | 10 | ## Track Sign Ups and Submissions 11 | 12 | You can track form submissions and sign ups. This allows you to test which variant of your content drives the most visitors to engage with your forms. 13 | 14 | {% asset_img html-forms-integration.jpg HTML Forms integration %} 15 | 16 | ## Integrations so Far 17 | 18 | As of now, A/B Testing for WordPress integrates with [Mailchimp for WordPress](https://wordpress.org/plugins/mailchimp-for-wp/), [HTML Forms](https://wordpress.org/plugins/html-forms/), and [Contact Form 7](https://wordpress.org/plugins/contact-form-7/). 19 | 20 | ## More to Come 21 | 22 | Other popular form plugins will be added soon. As their structure does not match A/B Testing for WordPress right now, it will need some more work to allow these plugins to be integrated. 23 | 24 | Gravity Forms, Formidable Forms, and Ninja Forms are planned to be implemented soon. 25 | 26 | ## How to Use 27 | 28 | Edit the page of the test you want to use with the integrated plugin. 29 | 30 | In the test settings, under "Testing Goal", select the plugin from the "Type" options dropdown. 31 | 32 | Now pick the form you want to set a a goal. 33 | 34 | Save / Update the post and you're all set! 35 | 36 | ## Update to Start Using 37 | 38 | Update your installed version of [A/B Testing for WordPress](https://wordpress.org/plugins/ab-testing-for-wp/) and start using these form integrations right now! 39 | -------------------------------------------------------------------------------- /blog/source/_posts/form-integrations/html-forms-integration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/form-integrations/html-forms-integration.jpg -------------------------------------------------------------------------------- /blog/source/_posts/new-form-integrations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: New Form Plugin Integrations 3 | date: 2019-04-09 10:00:00 4 | description: Added form plugin integrations for Ninja Forms, Formidable, Gravity Forms, and WPForms. 5 | --- 6 | Just a quick update on the form integrations: just added [WPForms](https://wordpress.org/plugins/wpforms-lite/), Gravity Forms, [Ninja Forms](https://wordpress.org/plugins/ninja-forms/), and [Formidable](https://wordpress.org/plugins/formidable/), as integrations. 7 | 8 | If you have any one of these plugins installed, you can pick your forms as a tracking goal for your test. 9 | 10 | ## Update to Start Using 11 | 12 | Update your installed version of [A/B Testing for WordPress](https://wordpress.org/plugins/ab-testing-for-wp/) and start using these new form integrations right now 13 | -------------------------------------------------------------------------------- /blog/source/_posts/place-visitors-variant-using-url-parameters.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Place Visitors in a Variant using URL Parameters 3 | date: 2020-02-13 20:00:00 4 | description: Through conditions, force a visitor to be placed in a variant of your test. 5 | --- 6 | 7 | A reoccurring feature request I got was the ability to place visitors in a predetermined variant of a test through the use of query parameters in the URL. 8 | 9 | This feature is great if you want to be able to have control over the variant of a test the visitor is placed in. 10 | 11 | This feature is now available in [A/B Testing for WordPress](https://wordpress.org/plugins/ab-testing-for-wp/). 12 | 13 | {% asset_img ab-test-conditions.png Setup variant conditions for A/B tests %} 14 | 15 | ## Placing visitors in a variant 16 | 17 | When you go to a test's settings you'll find a new option to add conditions under variants. 18 | 19 | Choose "add condition to A" to add a condition for variant A. Enter the key and value pair you want to force visitor into this variant. 20 | 21 | Key value pairs look like this in the URL of a page: `?key=value&another=thing`. Also known as query parameters. 22 | 23 | Once a visitor lands on a page with your test and has the key value pair in their URL, they will be placed in said variant. 24 | 25 | ## Integrates well with analytics and other marketing tools 26 | 27 | For your convenience `utm_source`, `utm_medium`, and `utm_campaign` are added to be quickly added as conditions. 28 | 29 | This way you can for instance show your newsletter readers a certain variant of a test _and_ also keep track of it in your analytics tool by using `utm_source=newsletter` as a condition. 30 | 31 | ## Upgrade now 32 | 33 | [Update A/B Testing for WordPress to 1.17.0](https://wordpress.org/plugins/ab-testing-for-wp/) to get this new feature. 34 | -------------------------------------------------------------------------------- /blog/source/_posts/place-visitors-variant-using-url-parameters/ab-test-conditions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/place-visitors-variant-using-url-parameters/ab-test-conditions.png -------------------------------------------------------------------------------- /blog/source/_posts/track-outbound-links.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Track Outbound Links 3 | date: 2020-01-27 14:00:00 4 | description: Track outbound links as a goal in A/B tests in WordPress 5 | --- 6 | From now on you can track **outbound links** in A/B Testing for WordPress. 7 | 8 | Now you're not bound to just pages and posts, but can also track if the user goes to a specific link. 9 | 10 | Find this addition in the latest version of [A/B Testing for WordPress](https://wordpress.org/plugins/ab-testing-for-wp/). 11 | 12 | {% asset_img screenshot.png Track outbound link as goal for A/B tests %} 13 | 14 | ## Setup Outbound Link Tracking 15 | 16 | 1. In the content editor, find the A/B test you wish to track outbound links. 17 | 2. Open the A/B tests' settings by clicking the cog. 18 | 3. Scroll down in the sidebar and select "Outbound link" under _Testing Goal_. 19 | 4. Enter the URL of the outbound link you want to track. 20 | 21 | Whenever a user click a link on your website which directs the user to this URL, they will be added as a successful conversion of said test. 22 | -------------------------------------------------------------------------------- /blog/source/_posts/track-outbound-links/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/_posts/track-outbound-links/screenshot.png -------------------------------------------------------------------------------- /blog/source/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/source/favicon.ico -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/_config.yml: -------------------------------------------------------------------------------- 1 | node_sass: 2 | outputStyle: compressed 3 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/archive.ejs: -------------------------------------------------------------------------------- 1 | <%- partial('partials/header') %> 2 |
3 |

Blog

4 | 5 |
6 | <% page.posts.each(function(post){ %> 7 |
8 | <%- partial('partials/post/title', { post, index: 1, class_name: '' }) %> 9 | <%- partial('partials/post/date', { post, date_format: null, prefix: '' }) %> 10 |
11 | <% }) %> 12 |
13 |
14 | <%- partial('partials/footer') %> 15 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%- partial('partials/head') %> 4 | 5 | <%- body -%> 6 | 7 | 8 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/page.ejs: -------------------------------------------------------------------------------- 1 | <%- partial('partials/page', { post: page, index: false }) %> 2 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/partials/article.ejs: -------------------------------------------------------------------------------- 1 | <%- partial('partials/header') %> 2 |
3 |
4 | <% if (post.link || post.title){ %> 5 |
6 | <%- partial('post/title', {class_name: 'article-title'}) %> 7 |
8 | <% } %> 9 | 12 |
13 | <% if (post.excerpt && index){ %> 14 | <%- post.excerpt %> 15 | <% if (theme.excerpt_link){ %> 16 |

17 | <%= theme.excerpt_link %> 18 |

19 | <% } %> 20 | <% } else { %> 21 | <%- post.content %> 22 | <% } %> 23 |
24 |
25 |
26 | <%- partial('partials/footer') %> 27 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/partials/head.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | var pageTitle = is_archive() ? 'Blog' : page.title; 3 | var title = pageTitle ? `${pageTitle} — ${config.title}` : `${config.title} — by CleverNode`; 4 | var description = page.description ? page.description : config.description; 5 | %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= title %> 14 | 15 | 16 | <%- css('css/style') %> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/partials/header.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | A/B Testing for WordPress logo 5 |

A/B Testing for WordPress

6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/partials/page.ejs: -------------------------------------------------------------------------------- 1 | <%- partial('partials/header') %> 2 |
3 |
4 | <% if (post.link || post.title){ %> 5 |
6 | <%- partial('post/title', {class_name: 'article-title'}) %> 7 |
8 | <% } %> 9 |
10 | <%- post.content %> 11 |
12 |
13 |
14 | <%- partial('partials/footer') %> 15 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/partials/post/date.ejs: -------------------------------------------------------------------------------- 1 |
2 | <%- prefix %> 3 |
4 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/partials/post/title.ejs: -------------------------------------------------------------------------------- 1 | <% if (post.link){ %> 2 |

3 | 4 |

5 | <% } else if (post.title){ %> 6 | <% if (index){ %> 7 |

8 | <%= post.title %> 9 |

10 | <% } else { %> 11 |

12 | <%= post.title %> 13 |

14 | <% } %> 15 | <% } %> -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/layout/post.ejs: -------------------------------------------------------------------------------- 1 | <%- partial('partials/article', { post: page, index: false }) %> 2 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/css/_article.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .archive, 4 | .article { 5 | padding: 3em 3%; 6 | margin: 0 auto; 7 | max-width: 600px; 8 | } 9 | 10 | .article-header { 11 | h1 { 12 | font-size: 2em; 13 | line-height: 1.5em; 14 | } 15 | } 16 | 17 | .article-date { 18 | color: lighten($body, 50); 19 | margin: 0.5em 0; 20 | } 21 | 22 | .article-entry { 23 | img { 24 | width: 100%; 25 | } 26 | 27 | p { 28 | margin: 1em 0; 29 | } 30 | 31 | a { 32 | color: $ab-blue; 33 | 34 | &:hover { 35 | color: darken($ab-blue, 10); 36 | } 37 | } 38 | 39 | pre { 40 | background: darken($bg, 4); 41 | padding: 0.3em; 42 | } 43 | 44 | code { 45 | font-family: monospace; 46 | } 47 | 48 | ol { 49 | li { 50 | list-style-type: decimal; 51 | list-style-position: inside; 52 | margin: 0.5em 0; 53 | } 54 | } 55 | } 56 | 57 | @media only screen and (min-width: $screen-md) { 58 | .article-entry { 59 | img { 60 | width: calc(100% + 2em); 61 | margin: 1em -1em; 62 | } 63 | } 64 | } 65 | 66 | .archive-post { 67 | display: flex; 68 | margin: 0.5em 0; 69 | 70 | .article-date { 71 | margin: 0; 72 | } 73 | 74 | h1 a { 75 | color: $ab-blue; 76 | margin-right: 5px; 77 | 78 | &:hover { 79 | color: darken($ab-blue, 10); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/css/_footer.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | footer { 4 | background: #323232; 5 | color: darken($bg, 25); 6 | text-align: center; 7 | padding: 2em 3%; 8 | font-size: 0.9em; 9 | 10 | .menu { 11 | ul { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | li { 18 | &:after { 19 | margin-right: 4.5px; 20 | content: "—"; 21 | } 22 | 23 | &:last-child{ 24 | margin-right: 0; 25 | 26 | &:after { 27 | content: ""; 28 | } 29 | } 30 | } 31 | } 32 | 33 | .content { 34 | font-style: italic; 35 | } 36 | 37 | a { 38 | color: $bg; 39 | 40 | &:hover { 41 | color: darken($bg, 25); 42 | } 43 | } 44 | 45 | p { 46 | margin: 1em 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/css/_frontpage.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .usp { 4 | margin: 3em 0; 5 | 6 | ul { 7 | max-width: $max-container; 8 | margin: 0 auto; 9 | padding: 0 3%; 10 | 11 | li { 12 | display: flex; 13 | align-items: center; 14 | margin: 3em 0; 15 | width: 100%; 16 | 17 | &:nth-child(2n) { 18 | .icon { 19 | order: 2; 20 | margin-left: 25px; 21 | margin-right: 0; 22 | } 23 | } 24 | } 25 | } 26 | 27 | .icon { 28 | width: 100px; 29 | height: 100px; 30 | margin-right: 25px; 31 | 32 | &.rounded { 33 | padding: 10px; 34 | border-radius: 10px; 35 | width: 80px; 36 | height: 80px; 37 | } 38 | 39 | &.green { 40 | background-color: #199333; 41 | } 42 | 43 | &.blue { 44 | background-color: $ab-blue; 45 | } 46 | } 47 | 48 | .content { 49 | width: 100%; 50 | } 51 | } 52 | 53 | .example-1 { 54 | margin: 4em auto; 55 | width: 100%; 56 | max-width: $max-container + 120; 57 | 58 | img { 59 | width: 100%; 60 | } 61 | 62 | h2 { 63 | text-align: center; 64 | margin-bottom: 2em; 65 | } 66 | } 67 | 68 | .example-2, 69 | .example-3 { 70 | padding: 3em 0; 71 | background: darken(#fff, 3); 72 | 73 | .container { 74 | margin: 0 auto; 75 | max-width: $max-container; 76 | padding: 0 3%; 77 | } 78 | 79 | img { 80 | display: block; 81 | width: calc(100% - 2em); 82 | padding: 10px; 83 | background: #fff; 84 | } 85 | 86 | p { 87 | margin: 1em 0; 88 | } 89 | } 90 | 91 | .example-3 { 92 | img { 93 | padding: 30px; 94 | } 95 | } 96 | 97 | .download { 98 | display: flex; 99 | justify-content: center; 100 | padding: 1em 2em; 101 | } 102 | 103 | .faq { 104 | margin: 3em 0; 105 | 106 | .container { 107 | margin: 0 auto; 108 | max-width: $max-container; 109 | padding: 0 3%; 110 | } 111 | 112 | p { 113 | margin: 1em 0; 114 | } 115 | } 116 | 117 | @media only screen and (min-width: $screen-md) { 118 | .header .monitor { 119 | position: absolute; 120 | width: 43%; 121 | left: 55%; 122 | top: 3em; 123 | max-width: 600px; 124 | } 125 | 126 | .usp { 127 | ul { 128 | display: flex; 129 | flex-direction: row; 130 | flex-wrap: wrap; 131 | justify-content: space-between; 132 | 133 | li, li:nth-child(2n) { 134 | width: 47%; 135 | 136 | .icon { 137 | order: 0; 138 | margin-right: 25px; 139 | margin-left: 0; 140 | } 141 | } 142 | } 143 | } 144 | 145 | .example-3, 146 | .example-2 { 147 | .container { 148 | display: flex; 149 | align-items: center; 150 | } 151 | 152 | .content { 153 | margin-right: 2em; 154 | width: 40%; 155 | flex-shrink: 0; 156 | } 157 | } 158 | 159 | .example-3 { 160 | .content { 161 | order: 2; 162 | margin-left: 2em; 163 | margin-right: 0; 164 | width: 60%; 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/css/_header.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .download, 4 | .header { 5 | background: { 6 | color: $ab-blue; 7 | position: 50% 50%; 8 | image: repeating-linear-gradient( 9 | 315deg, 10 | $ab-blue, 11 | $ab-blue 8px, 12 | $ab-blue-darker 8px, 13 | $ab-blue-darker 13px 14 | ); 15 | } 16 | color: $ab-light; 17 | 18 | .container { 19 | margin: 0 auto; 20 | max-width: $max-container; 21 | padding: 3em 5%; 22 | } 23 | 24 | .container-text { 25 | max-width: 400px; 26 | } 27 | 28 | .monitor { 29 | width: 100%; 30 | height: auto; 31 | margin-top: 1em; 32 | } 33 | 34 | img { 35 | display: block; 36 | } 37 | 38 | h1 { 39 | font-size: 2.4em; 40 | line-height: 1.3em; 41 | margin: 0.5em 0; 42 | font-weight: bold; 43 | max-width: 290px; 44 | } 45 | 46 | p { 47 | margin: 1em 0; 48 | } 49 | 50 | .cta { 51 | color: $ab-light; 52 | text-decoration: none; 53 | font-weight: bold; 54 | padding: 0.8em 1.8em; 55 | display: inline-block; 56 | margin: 1em 0; 57 | border: 3px solid $ab-light; 58 | transition: color 0.2s ease, background-color 0.2s ease; 59 | text-align: center; 60 | 61 | &:hover { 62 | background: $ab-light; 63 | color: $ab-blue; 64 | } 65 | } 66 | 67 | a { 68 | text-decoration: none; 69 | display: flex; 70 | align-items: center; 71 | color: $ab-light; 72 | } 73 | 74 | &--small { 75 | padding: 1em 3%; 76 | 77 | .container { 78 | padding: 0; 79 | max-width: 600px; 80 | } 81 | 82 | h1 { 83 | max-width: none; 84 | margin: 0 0 0 0.5em; 85 | font-size: 1.4em; 86 | } 87 | 88 | a:hover { 89 | text-decoration: underline; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/css/_reset.scss: -------------------------------------------------------------------------------- 1 | // http://meyerweb.com/eric/tools/css/reset/ 2 | // v2.0 | 20110126 3 | // License: none (public domain) 4 | 5 | @mixin meyer-reset { 6 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 7 | margin: 0; 8 | padding: 0; 9 | border: 0; 10 | font-size: 100%; 11 | font: inherit; 12 | vertical-align: baseline; 13 | } 14 | 15 | // HTML5 display-role reset for older browsers 16 | article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { 17 | display: block; 18 | } 19 | body { 20 | line-height: 1; 21 | } 22 | ol, ul { 23 | list-style: none; 24 | } 25 | blockquote, q { 26 | quotes: none; 27 | } 28 | blockquote { 29 | &:before, &:after { 30 | content: ""; 31 | content: none; 32 | } 33 | } 34 | q { 35 | &:before, &:after { 36 | content: ""; 37 | content: none; 38 | } 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | } 45 | 46 | @include meyer-reset; 47 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $screen-md: 760px; 2 | $max-container: 960px; 3 | 4 | $bg: #fff; 5 | $body: #111; 6 | 7 | $ab-light: #f5f5ff; 8 | $ab-blue: #268ec7; 9 | $ab-blue-darker: #268bc3; 10 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/css/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'reset'; 3 | @import 'frontpage'; 4 | @import 'header'; 5 | @import 'footer'; 6 | @import 'article'; 7 | 8 | body { 9 | background: $bg; 10 | color: $body; 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 12 | line-height: 1.7em; 13 | font-size: 18px; 14 | overflow-x: hidden; 15 | } 16 | 17 | strong { 18 | font-weight: bold; 19 | } 20 | 21 | em { 22 | font-style: italic; 23 | } 24 | 25 | h2 { 26 | font-size: 1.3em; 27 | margin-bottom: 0.3em; 28 | font-weight: 600; 29 | } 30 | 31 | h3 { 32 | font-size: 1.1em; 33 | font-weight: 500; 34 | } 35 | 36 | body p a { 37 | color: $ab-blue; 38 | 39 | &:hover { 40 | color: darken($ab-blue, 20); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/campaign-tweaking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/images/favicon-16x16.png -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/images/favicon-32x32.png -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/financial-analysis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/integrations.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/images/integrations.jpg -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/market-research.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/og_abtestingforwp_poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/images/og_abtestingforwp_poster.jpg -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/screenshot-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/images/screenshot-1.gif -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/screenshot-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/images/screenshot-2.gif -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/images/screenshot-3.jpg -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/seo-report.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/web-development.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/images/web-protection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 27 | 29 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /blog/themes/ab-testing-for-wp/source/integrations.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/blog/themes/ab-testing-for-wp/source/integrations.psd -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "classmap": [ 4 | "src/php" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportFile": "cypress/support/index.ts", 3 | "baseUrl": "http://localhost:9000", 4 | "env": { 5 | "WP_USER": "test_user", 6 | "WP_PASSWORD": "test_password_123" 7 | }, 8 | "fixturesFolder": false, 9 | "projectId": "7yzjry" 10 | } 11 | -------------------------------------------------------------------------------- /cypress/integration/admin-bar.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Admin bar', () => { 2 | before(() => { 3 | cy.cleanInstall(); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.login(); 8 | cy.disableTooltips(); 9 | }); 10 | 11 | afterEach(() => { 12 | cy.logout(); 13 | }); 14 | 15 | it('Shows current tests on page in admin bar and is able to change variants', () => { 16 | // create new post 17 | cy.visitAdmin('post-new.php?skipOnboarding=1'); 18 | 19 | // add default test 20 | cy.addBlockInEditor('A/B Test', 'Test on page'); 21 | 22 | // save post 23 | cy.savePost(); 24 | 25 | // go to post 26 | cy.get('.post-publish-panel__postpublish-buttons > a.components-button') 27 | .click(); 28 | 29 | // should contain test in admin bar 30 | cy.contains('A/B Tests (1)') 31 | .wait(200) 32 | .click(); 33 | 34 | // open test variants 35 | cy.contains('Test on page'); 36 | 37 | // pick variant B 38 | cy.get('button.ab-item') 39 | .eq(1) 40 | .click({ force: true }); 41 | 42 | // check if B loads 43 | cy.contains('Button for Test Variant “B”'); 44 | 45 | // pick variant A 46 | cy.get('button.ab-item') 47 | .eq(0) 48 | .click({ force: true }); 49 | 50 | // check if A loads 51 | cy.contains('Button for Test Variant “A”'); 52 | }); 53 | 54 | it('Shows two tests on page in admin bar', () => { 55 | // create new post 56 | cy.visitAdmin('post-new.php?skipOnboarding=1'); 57 | 58 | // add two default tests 59 | cy.addBlockInEditor('A/B Test', 'Test on page 1'); 60 | cy.addBlockInEditor('A/B Test', 'Test on page 2'); 61 | 62 | // save post 63 | cy.savePost(); 64 | 65 | // go to post 66 | cy.get('.post-publish-panel__postpublish-buttons > a.components-button') 67 | .click(); 68 | 69 | // should contain test in admin bar 70 | cy.contains('A/B Tests (2)') 71 | .wait(200) 72 | .click(); 73 | 74 | // has both variants 75 | cy.contains('Test on page 1'); 76 | cy.contains('Test on page 2'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /cypress/integration/how-to.spec.ts: -------------------------------------------------------------------------------- 1 | describe('How to Use page', () => { 2 | before(() => { 3 | cy.cleanInstall(); 4 | }); 5 | 6 | it('Check if how to page loads', () => { 7 | cy.login(); 8 | 9 | cy.visitAdmin(); 10 | 11 | cy.contains('A/B Testing') 12 | .click(); 13 | cy.contains('How to Use') 14 | .click(); 15 | 16 | cy.contains('How to Use A/B Testing for WordPress'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /cypress/integration/onboarding.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Onboarding', () => { 2 | beforeEach(() => { 3 | cy.cleanInstall(); 4 | cy.login(); 5 | cy.disableTooltips(); 6 | }); 7 | 8 | afterEach(() => { 9 | cy.logout(); 10 | }); 11 | 12 | function checkStep(text: string, gotoNext = true): void { 13 | cy.contains(text); 14 | 15 | if (!gotoNext) return; 16 | 17 | // next step 18 | cy.get('.ab-testing-for-wp__Onboarding button.next') 19 | .click({ force: true, multiple: true }); 20 | } 21 | 22 | it('Loads onboarding on adding A/B Test for the first time', () => { 23 | cy.visitAdmin('post-new.php'); 24 | 25 | // add default test 26 | cy.addBlockInEditor('A/B Test'); 27 | 28 | // Shows onboarding modal 29 | cy.contains('Welcome to A/B Testing for WordPress!'); 30 | 31 | // start tour 32 | cy.get('.ButtonContainer > .is-primary') 33 | .click(); 34 | 35 | // check if step 1 loads 36 | checkStep('edit your test variants'); 37 | 38 | // check if step 2 loads 39 | checkStep('Switch between editing variants'); 40 | 41 | // check if step 3 loads 42 | checkStep('Use the cog toggle'); 43 | 44 | // check if step 4 loads 45 | checkStep('configure the test in this sidebar'); 46 | 47 | // check if step 5 loads 48 | checkStep('Select the goal'); 49 | 50 | // check if step 6 loads 51 | checkStep('the control version'); 52 | 53 | // check if step 7 loads 54 | checkStep('adjust the distribution weight'); 55 | 56 | // check if step 8 loads 57 | checkStep('enable and run the test'); 58 | 59 | // Tour finished! 60 | cy.contains('That is it!'); 61 | 62 | // Finish tour 63 | cy.get('.ButtonContainer > .is-primary') 64 | .click(); 65 | }); 66 | 67 | it('Can cancel the tour and stays closed', () => { 68 | cy.visitAdmin('post-new.php'); 69 | 70 | // add default test 71 | cy.addBlockInEditor('A/B Test'); 72 | 73 | // cancel tour 74 | cy.get('.ButtonContainer > .is-link') 75 | .click(); 76 | }); 77 | 78 | it('Should not load onboarding once it has been dismissed', () => { 79 | cy.visitAdmin('post-new.php'); 80 | 81 | // add default test 82 | cy.addBlockInEditor('A/B Test'); 83 | 84 | // cancel tour 85 | cy.get('.ButtonContainer > .is-link') 86 | .click(); 87 | 88 | // open add post page again 89 | cy.visitAdmin('post-new.php'); 90 | 91 | // add default test 92 | cy.addBlockInEditor('A/B Test'); 93 | 94 | // open add new test (will not happen if onbaording is opened) 95 | cy.get('.edit-post-header-toolbar > .block-editor-inserter > .components-button') 96 | .click(); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /cypress/integration/overview.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Overview of created tests', () => { 2 | before(() => { 3 | cy.cleanInstall(); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.login(); 8 | cy.disableTooltips(); 9 | }); 10 | 11 | afterEach(() => { 12 | cy.logout(); 13 | }); 14 | 15 | it('Lists inline tests created in posts / pages', () => { 16 | // create new post 17 | cy.visitAdmin('post-new.php?skipOnboarding=1'); 18 | 19 | // add default test 20 | cy.addBlockInEditor('A/B Test', 'Inline A/B Test'); 21 | 22 | // Change target post to "Hello World!" 23 | cy.get('#inspector-select-control-3') 24 | .select('Hello world!'); 25 | 26 | // save post 27 | cy.savePost(); 28 | 29 | // open A/B Testing menu 30 | cy.contains('A/B Testing') 31 | .click(); 32 | 33 | // shows test in list 34 | cy.contains('Inline A/B Test'); 35 | 36 | // shows test goal 37 | cy.contains('Hello world!'); 38 | }); 39 | 40 | it('Lists stand alone tests', () => { 41 | cy.visitAdmin('post-new.php?post_type=abt4wp-test&skipOnboarding=1'); 42 | 43 | // wait for test to get focus 44 | cy.focusBlock(); 45 | 46 | // fill in title 47 | cy.get('#post-title-0') 48 | .type('Stand alone test', { force: true }); 49 | 50 | // save test 51 | cy.savePost(); 52 | 53 | // open A/B Testing menu 54 | cy.contains('A/B Testing') 55 | .click(); 56 | 57 | // shows test in list 58 | cy.contains('Stand alone test'); 59 | }); 60 | 61 | it('Can start and stop tests from the overview', () => { 62 | cy.visitAdmin('post-new.php?post_type=abt4wp-test&skipOnboarding=1'); 63 | 64 | // wait for test to get focus 65 | cy.focusBlock(); 66 | 67 | // fill in title 68 | cy.get('#post-title-0') 69 | .type('Start me test', { force: true }); 70 | 71 | // save test 72 | cy.savePost(); 73 | 74 | // open A/B Testing menu 75 | cy.contains('A/B Testing') 76 | .click(); 77 | 78 | // start the test 79 | cy.contains('Start me test') 80 | .parent() 81 | .parent() 82 | .parent() 83 | .contains('Run test') 84 | .click({ force: true }); 85 | 86 | // indicator should turn on 87 | cy.contains('Start me test') 88 | .parent() 89 | .parent() 90 | .parent() 91 | .get('.indicator--on'); 92 | 93 | // stop test 94 | cy.contains('Start me test') 95 | .parent() 96 | .parent() 97 | .parent() 98 | .contains('Stop test') 99 | .click({ force: true }); 100 | 101 | // indicator should be off 102 | cy.contains('Start me test') 103 | .parent() 104 | .parent() 105 | .parent() 106 | .get('.indicator--off'); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /cypress/integration/plugin-activation.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Plugin activation', () => { 2 | before(() => { 3 | cy.wipeInstall(); 4 | cy.installWordPress(); 5 | }); 6 | 7 | it('Successfully loads without plugin installed', () => { 8 | cy.visit('/'); 9 | }); 10 | 11 | it('Successfully loads with plugin installed', () => { 12 | cy.activatePlugin(); 13 | 14 | // see if front page still works 15 | cy.visit('/'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /cypress/integration/stand-alone.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Stand alone A/B tests', () => { 2 | before(() => { 3 | cy.cleanInstall(); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.login(); 8 | cy.disableTooltips(); 9 | }); 10 | 11 | afterEach(() => { 12 | cy.logout(); 13 | }); 14 | 15 | function createStandAloneTest(name: string): void { 16 | cy.visitAdmin('post-new.php?post_type=abt4wp-test&skipOnboarding=1'); 17 | 18 | // wait for test to get focus 19 | cy.focusBlock(); 20 | 21 | // fill in title 22 | cy.get('#post-title-0') 23 | .type(name, { force: true }); 24 | 25 | // save test 26 | cy.savePost(); 27 | 28 | // wait for save message 29 | cy.contains('Post published.'); 30 | } 31 | 32 | it('Should allow the user to create a stand alone test', () => { 33 | createStandAloneTest('This is a stand alone test'); 34 | 35 | // reload and skip onboarding 36 | cy.location() 37 | .then(({ pathname, search }) => { 38 | cy.visit(`${pathname}${search}&skipOnboarding=1`); 39 | }); 40 | }); 41 | 42 | it('Should be able to pick stand alone tests when adding A/B test in content', () => { 43 | const TEST_TITLE = 'Stand alone in content'; 44 | 45 | createStandAloneTest(TEST_TITLE); 46 | 47 | // go to new post create page 48 | cy.visitAdmin('post-new.php?skipOnboarding=1'); 49 | 50 | // add default test 51 | cy.addBlockInEditor('A/B Test'); 52 | 53 | // should see popup 54 | cy.get('.components-modal__content'); 55 | 56 | // choose to insert existing 57 | cy.get('.Inserter__actions > .is-secondary') 58 | .click(); 59 | 60 | // select correct test 61 | cy.get('#inspector-select-control-0') 62 | .select(TEST_TITLE); 63 | 64 | // add to content 65 | cy.get('.Inserter__picking > .is-primary') 66 | .click() 67 | .wait(500); 68 | 69 | // Click on test to activate 70 | cy.get('.EditWrapper__Overlay') 71 | .click(); 72 | 73 | // check if editing page is loaded 74 | cy.contains(TEST_TITLE); 75 | }); 76 | 77 | it('Can choose to add inline test when adding to content', () => { 78 | createStandAloneTest('Dummy test to pop up choice'); 79 | 80 | // go to new post create page 81 | cy.visitAdmin('post-new.php?skipOnboarding=1'); 82 | 83 | // add default test 84 | cy.addBlockInEditor('A/B Test'); 85 | 86 | // should see popup 87 | cy.get('.components-modal__content'); 88 | 89 | // choose to insert new 90 | cy.get('.Inserter__actions > .is-primary') 91 | .click(); 92 | 93 | // should have added standard test 94 | cy.contains('Button for Test Variant "A"'); 95 | }); 96 | 97 | it('Can use shortcode of stand alone test', () => { 98 | createStandAloneTest('Shortcode test A/B Test'); 99 | 100 | // close publish side bar 101 | cy.get('.edit-post-sidebar-header > .components-button') 102 | .click(); 103 | 104 | // open options 105 | cy.get('.components-button[aria-label=Settings]') 106 | .click(); 107 | 108 | // open test options 109 | cy.get('.components-button-group > :nth-child(3)') 110 | .click(); 111 | 112 | // grab shortcode 113 | cy.get('code') 114 | .then((code) => { 115 | const codeText = code.text(); 116 | 117 | // go to new post create page 118 | cy.visitAdmin('post-new.php?skipOnboarding=1'); 119 | 120 | // add shortcode block 121 | cy.addBlockInEditor('Shortcode'); 122 | 123 | // input shortcode 124 | cy.get('#blocks-shortcode-input-0') 125 | .type(codeText); 126 | }); 127 | 128 | // save content 129 | cy.savePost(); 130 | 131 | // go to post 132 | cy.get('.post-publish-panel__postpublish-buttons > a.components-button') 133 | .click(); 134 | 135 | // should have added standard test 136 | cy.contains('Button for Test Variant “A”'); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const webpack = require('@cypress/webpack-preprocessor'); 2 | 3 | module.exports = (on) => { 4 | on('file:preprocessor', webpack({ 5 | webpackOptions: require('../../webpack.config'), 6 | })); 7 | }; 8 | -------------------------------------------------------------------------------- /cypress/support/index.ts: -------------------------------------------------------------------------------- 1 | import './patches'; 2 | import './commands'; 3 | -------------------------------------------------------------------------------- /cypress/support/patches.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | function patchLeaveMessage(win: any): void { 3 | // get rid of the "Stay on page" unsaved changes messages blocking navigation in tests 4 | const original = win.EventTarget.prototype.addEventListener; 5 | 6 | // eslint-disable-next-line no-param-reassign,@typescript-eslint/no-explicit-any 7 | win.EventTarget.prototype.addEventListener = function catchEvent(): any { 8 | // eslint-disable-next-line prefer-rest-params 9 | if (arguments && arguments[0] === 'beforeunload') { 10 | return; 11 | } 12 | 13 | // eslint-disable-next-line prefer-rest-params,consistent-return 14 | return original.apply(this, arguments); 15 | }; 16 | 17 | Object.defineProperty(win, 'onbeforeunload', { 18 | get() { 19 | // silence 20 | return undefined; 21 | }, 22 | set() { 23 | // silence 24 | return undefined; 25 | }, 26 | }); 27 | } 28 | 29 | Cypress.on('window:before:load', (win) => { 30 | patchLeaveMessage(win); 31 | }); 32 | -------------------------------------------------------------------------------- /cypress/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Cypress { 2 | interface Chainable { 3 | login(): void; 4 | logout(): void; 5 | wipeABTestingCookies(): void; 6 | visitAdmin(subpage?: string): void; 7 | addBlockInEditor(search: string, name?: string): void; 8 | savePost(): void; 9 | focusBlock(eq?: number): void; 10 | disableTooltips(): void; 11 | changeRange(selector: string, value: number): void; 12 | installWordPress(): void; 13 | activatePlugin(): void; 14 | wipeInstall(): void; 15 | cleanInstall(): void; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose-e2e.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | db_e2e: 5 | image: mysql:5.7 6 | volumes: 7 | - db_data_e2e:/var/lib/mysql 8 | restart: always 9 | environment: 10 | MYSQL_ROOT_PASSWORD: root_wordpress 11 | MYSQL_DATABASE: wordpress_e2e 12 | MYSQL_USER: wordpress 13 | MYSQL_PASSWORD: wordpress 14 | ports: 15 | - "3377:3306" 16 | 17 | wordpress_e2e: 18 | depends_on: 19 | - db_e2e 20 | image: wordpress:5.4.0-php7.4-apache 21 | ports: 22 | - "9000:80" 23 | restart: always 24 | environment: 25 | WORDPRESS_DB_HOST: db_e2e:3306 26 | WORDPRESS_DB_USER: wordpress 27 | WORDPRESS_DB_PASSWORD: wordpress 28 | WORDPRESS_DB_NAME: wordpress_e2e 29 | WORDPRESS_DEBUG: 1 30 | volumes: 31 | - ./wp-content-e2e:/var/www/html/wp-content 32 | - ./:/var/www/html/wp-content/plugins/ab-testing-for-wp 33 | 34 | volumes: 35 | db_data_e2e: {} 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | volumes: 7 | - db_data:/var/lib/mysql 8 | restart: always 9 | environment: 10 | MYSQL_ROOT_PASSWORD: root_wordpress 11 | MYSQL_DATABASE: wordpress 12 | MYSQL_USER: wordpress 13 | MYSQL_PASSWORD: wordpress 14 | ports: 15 | - "3366:3306" 16 | 17 | wordpress: 18 | depends_on: 19 | - db 20 | image: wordpress:latest 21 | ports: 22 | - "8000:80" 23 | restart: always 24 | environment: 25 | WORDPRESS_DB_HOST: db:3306 26 | WORDPRESS_DB_USER: wordpress 27 | WORDPRESS_DB_PASSWORD: wordpress 28 | WORDPRESS_DB_NAME: wordpress 29 | WORDPRESS_DEBUG: 1 30 | volumes: 31 | - ./wp-content:/var/www/html/wp-content 32 | - ./:/var/www/html/wp-content/plugins/ab-testing-for-wp 33 | 34 | volumes: 35 | db_data: {} 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ab-test-wordpress", 3 | "version": "1.18.2", 4 | "description": "A/B Test right from your WordPress posts.", 5 | "main": "index.js", 6 | "scripts": { 7 | "e2e:open": "cypress open", 8 | "e2e:run": "cypress run", 9 | "e2e:docker-compose": "docker-compose -p ab-testing-for-wp-e2e -f docker-compose-e2e.yml", 10 | "e2e:down": "npm run e2e:docker-compose -- down -v", 11 | "e2e:up": "npm run e2e:docker-compose -- up -d", 12 | "e2e:wipe-db": "npm run e2e:docker-compose -- exec -T db_e2e mysql -u root --password=root_wordpress -e \"DROP DATABASE wordpress_e2e; CREATE DATABASE wordpress_e2e;\"", 13 | "e2e:reset-db": "npm run e2e:docker-compose -- exec -T db_e2e mysql -u root --password=root_wordpress wordpress_e2e < cypress/data/wordpress_e2e_2020-04-06.sql", 14 | "e2e:setup-env": "npm run e2e:down && npm run e2e:up && wait-on http://localhost:9000", 15 | "pretest": "npm run clean-build && npm run e2e:setup-env", 16 | "test": "npm run e2e:run", 17 | "posttest": "npm run e2e:down", 18 | "pretest:dev": "npm run e2e:setup-env && npm run e2e:reset-db", 19 | "test:dev": "npm run e2e:open", 20 | "lint": "eslint '**/*.ts' '**/*.tsx' '**/*.js'", 21 | "tsc": "tsc", 22 | "build": "NODE_ENV=production webpack", 23 | "dev": "webpack --watch", 24 | "archive": "zip -r ab-testing-for-wp.zip ./ -x 'node_modules/*' -x '.git/*' -x 'wp-content/*' -x 'blog/*' -x 'svn/*' -x '.idea/*' -x '*.DS_Store' -x '.wordpress-org/*'", 25 | "clean": "rm -rf ./dist && rm -rf ab-testing-for-wp.zip", 26 | "composer": "composer dump-autoload", 27 | "bump-version": "sh scripts/bump.sh", 28 | "release": "sh scripts/release.sh", 29 | "clean-build": "npm run clean && npm run build", 30 | "analyze": "webpack --json > stats.json" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/Gaya/ab-testing-for-wp.git" 35 | }, 36 | "author": "Gaya Kessler", 37 | "license": "GPL-3.0-or-later", 38 | "bugs": { 39 | "url": "https://github.com/Gaya/ab-testing-for-wp/issues" 40 | }, 41 | "homepage": "https://github.com/Gaya/ab-testing-for-wp#readme", 42 | "devDependencies": { 43 | "@babel/cli": "^7.8.4", 44 | "@babel/core": "^7.9.0", 45 | "@babel/plugin-proposal-class-properties": "^7.8.3", 46 | "@babel/plugin-transform-react-jsx": "^7.9.4", 47 | "@babel/preset-env": "^7.9.5", 48 | "@babel/preset-react": "^7.9.4", 49 | "@babel/preset-typescript": "^7.9.0", 50 | "@types/classnames": "^2.2.10", 51 | "@types/react": "^16.9.34", 52 | "@types/scroll-into-view": "^1.6.7", 53 | "@types/shortid": "0.0.29", 54 | "@types/wordpress__api-fetch": "^3.2.2", 55 | "@types/wordpress__block-editor": "^2.2.6", 56 | "@types/wordpress__blocks": "^6.4.5", 57 | "@types/wordpress__components": "^8.5.0", 58 | "@types/wordpress__compose": "^3.4.0", 59 | "@types/wordpress__data": "^4.6.6", 60 | "@types/wordpress__edit-post": "^3.5.1", 61 | "@types/wordpress__i18n": "^3.4.0", 62 | "@types/wordpress__plugins": "^2.3.4", 63 | "@typescript-eslint/eslint-plugin": "^2.28.0", 64 | "@typescript-eslint/parser": "^2.28.0", 65 | "babel-eslint": "^10.1.0", 66 | "babel-loader": "^8.1.0", 67 | "css-loader": "^3.5.2", 68 | "cypress": "^4.4.0", 69 | "eslint": "^6.8.0", 70 | "eslint-config-airbnb": "^18.1.0", 71 | "eslint-plugin-cypress": "^2.10.3", 72 | "eslint-plugin-import": "^2.20.2", 73 | "eslint-plugin-jsx-a11y": "^6.2.3", 74 | "eslint-plugin-react": "^7.19.0", 75 | "eslint-plugin-react-hooks": "^3.0.0", 76 | "redux-multi": "^0.1.12", 77 | "style-loader": "^1.1.2", 78 | "typescript": "^3.8.3", 79 | "wait-on": "^4.0.2", 80 | "webpack": "^4.42.1", 81 | "webpack-cli": "^3.3.11" 82 | }, 83 | "dependencies": { 84 | "@cypress/webpack-preprocessor": "^4.1.5", 85 | "@wordpress/api-fetch": "^3.12.0", 86 | "@wordpress/block-editor": "^3.8.0", 87 | "@wordpress/blocks": "^6.13.0", 88 | "@wordpress/components": "^9.3.0", 89 | "@wordpress/compose": "^3.12.0", 90 | "@wordpress/data": "^4.15.0", 91 | "@wordpress/edit-post": "^3.14.0", 92 | "@wordpress/i18n": "^3.10.0", 93 | "@wordpress/plugins": "^2.13.0", 94 | "classnames": "^2.2.6", 95 | "date-fns": "^2.12.0", 96 | "query-string": "^6.12.1", 97 | "react": "^16.13.1", 98 | "react-dom": "^16.13.1", 99 | "scroll-into-view": "^1.14.2", 100 | "shortid": "^2.2.15" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /scripts/bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | read -p "Enter version new: " VERSION 4 | echo "Version is" $VERSION 5 | 6 | # Add release numbers 7 | sed -i .copy -e "s/Stable tag: [0-9]*\.[0-9]*\.[0-9]*/Stable tag: $VERSION/" ./README.txt 8 | sed -i .copy -e "s/\"version\": \"[0-9]*\.[0-9]*\.[0-9]*\"/\"version\": \"$VERSION\"/" ./package.json 9 | sed -i .copy -e "s/@version [0-9]*\.[0-9]*\.[0-9]*/@version $VERSION/" ./ab-testing-for-wp.php 10 | sed -i .copy -e "s/Version: [0-9]*\.[0-9]*\.[0-9]*/Version: $VERSION/" ./ab-testing-for-wp.php 11 | 12 | rm *.copy 13 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=$(cat ab-testing-for-wp.php | grep "Version: \(.*\)" | tr ' ' '\n' | tail -1) 4 | 5 | echo "Release version $VERSION (y/n)?" 6 | read answer 7 | 8 | if [[ "$answer" != "${answer#[Yy]}" ]]; then 9 | echo "Releasing to GitHub..."; 10 | 11 | git tag $VERSION 12 | git push --tags 13 | else 14 | echo "Aborting"; 15 | fi; 16 | -------------------------------------------------------------------------------- /src/assets/ab-testing-for-wp-base64-logo.svg: -------------------------------------------------------------------------------- 1 | data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMb2dvIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB3aWR0aD0iMTAxLjJweCIgaGVpZ2h0PSIxMDEuMnB4IiB2aWV3Qm94PSIwIDAgMTAxLjIgMTAxLjIiIHN0eWxlPSJmaWxsOiNmZmYiIHhtbDpzcGFjZT0icHJlc2VydmUiCgk+Cjxwb2x5Z29uIHBvaW50cz0iMzkuOSw1OS4xIDI3LjMsODEuNCA0My4zLDgxLjQgNDAsNTkuMSAiLz4KPHBvbHlnb24gcG9pbnRzPSIxNS45LDEwMS4yIDQ2LjIsMTAxLjIgNDQuMiw4OC40IDIzLjQsODguNCAiLz4KPHBhdGggZD0iTTg5LjIsMEgxMkM1LjQsMCwwLDUuNCwwLDEydjc3LjJDMCw5NCwyLjgsOTguMSw2LjgsMTAwbDMwLjEtNTAuMmg5LjdsNS4xLDI5LjZsNi4xLTI5LjZoMjIuNmMzLjUsMCw2LjIsMC41LDguMywxLjQKCWMyLDAuOSwzLjYsMiw0LjYsMy4zYzEuMSwxLjMsMS43LDIuNiwyLDRjMC4zLDEuNCwwLjQsMi42LDAuNCwzLjVjMCwxLjYtMC4yLDMtMC43LDQuNGMtMC41LDEuNC0xLjEsMi43LTIsMy44CgljLTAuOSwxLjEtMS45LDIuMS0zLjIsMi44Yy0xLjIsMC43LTIuNiwxLjItNC4xLDEuNXYwLjFjNS4yLDEuNyw3LjksNS4zLDcuOSwxMWMwLDEuNC0wLjMsMy0wLjgsNC45Yy0wLjUsMS45LTEuNSwzLjctMyw1LjQKCWMtMS40LDEuNy0zLjUsMy4yLTYsNC40Yy0wLjYsMC4zLTEuMywwLjYtMi4xLDAuOGg3LjVjNi42LDAsMTItNS40LDEyLTEyVjEyQzEwMS4yLDUuNCw5NS44LDAsODkuMiwweiIvPgo8cGF0aCBkPSJNODEuNiw3MC45YzEuNC0wLjYsMi40LTEuMywzLjItMi4yYzAuNy0wLjksMS4yLTEuOCwxLjUtMi43YzAuMi0xLDAuNC0xLjgsMC40LTIuNWMwLTAuNS0wLjEtMS4yLTAuMi0xLjkKCWMtMC4xLTAuNy0wLjUtMS40LTEtMi4xQzg0LjgsNTksODQsNTguNCw4Mi45LDU4Yy0xLjEtMC40LTIuNS0wLjctNC40LTAuN0g2NS41bC0zLDE0LjVoMTMuN0M3OC40LDcxLjgsODAuMiw3MS41LDgxLjYsNzAuOXoiLz4KPHBhdGggZD0iTTU3LjcsOTQuOGgxMy4xYzAuOCwwLDEuOSwwLDMuMy0wLjFjMS40LDAsMi45LTAuMyw0LjMtMC44YzEuNS0wLjUsMi44LTEuMywzLjktMi40YzEuMS0xLjEsMS44LTIuOCwyLTQuOQoJYzAtMC42LDAtMS40LDAtMi4zYy0wLjEtMC45LTAuNC0xLjgtMS0yLjdjLTAuNi0wLjktMS42LTEuNi0yLjktMi4yYy0xLjMtMC42LTMuMS0wLjktNS41LTAuOUg2MS4xTDU3LjcsOTQuOHoiLz4KPC9zdmc+Cg== -------------------------------------------------------------------------------- /src/assets/ab-testing-for-wp-logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | -------------------------------------------------------------------------------- /src/assets/ab-testing-for-wp-logo-square.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/src/assets/ab-testing-for-wp-logo-square.eps -------------------------------------------------------------------------------- /src/assets/ab-testing-for-wp-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | -------------------------------------------------------------------------------- /src/assets/how-to-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/src/assets/how-to-1.png -------------------------------------------------------------------------------- /src/assets/how-to-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/src/assets/how-to-2.png -------------------------------------------------------------------------------- /src/assets/how-to-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/src/assets/how-to-3.png -------------------------------------------------------------------------------- /src/assets/how-to-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/src/assets/how-to-4.png -------------------------------------------------------------------------------- /src/assets/plugin-gutenberg-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJJ/ab-testing-for-wp/7a2d47487afa6aebc640d5c99817d0c9d69d16f5/src/assets/plugin-gutenberg-demo.gif -------------------------------------------------------------------------------- /src/css/admin-bar.css: -------------------------------------------------------------------------------- 1 | #wp-admin-bar-ab-testing-for-wp:hover > .ab-sub-wrapper, 2 | #wp-admin-bar-ab-testing-for-wp .menupop:hover > .ab-sub-wrapper { 3 | display: block; 4 | } 5 | 6 | #wpadminbar #wp-admin-bar-ab-testing-for-wp > .ab-item::before { 7 | content: ""; 8 | background-image: url('../assets/ab-testing-for-wp-logo-icon.svg') !important; 9 | background-repeat: no-repeat; 10 | background-size: 18px 18px; 11 | background-position: 50% 50%; 12 | top: 1px; 13 | width: 20px; 14 | height: 20px; 15 | display: inline-block; 16 | } 17 | 18 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__test > .ab-item { 19 | display: flex; 20 | align-items: center; 21 | } 22 | 23 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__test > .ab-item span { 24 | order: 2; 25 | } 26 | 27 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__test > .ab-item::after { 28 | content: ""; 29 | width: 10px; 30 | height: 10px; 31 | background: #d4d4d4; 32 | border-radius: 20px; 33 | display: inline-block; 34 | padding: 0; 35 | margin: 0 8px 0 0; 36 | order: 1; 37 | } 38 | 39 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__test.ab-testing-for-wp__enabled > .ab-item::after { 40 | background: #46B450; 41 | } 42 | 43 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__variant.ab-testing-for-wp__selected > .ab-item::after { 44 | content: "•"; 45 | margin-left: 5px; 46 | } 47 | 48 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__variant > .ab-item { 49 | cursor: pointer; 50 | } 51 | 52 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__variant button { 53 | text-align: left; 54 | background: none; 55 | } 56 | 57 | #wpadminbar #wp-admin-bar-ab-testing-for-wp .ab-testing-for-wp__test .ab-item:hover { 58 | color: #00b9eb; 59 | } 60 | 61 | .ab-testing-for-wp__highlight { 62 | animation-name: pulse_animation; 63 | animation-duration: 700ms; 64 | transform-origin: 70% 70%; 65 | animation-iteration-count: infinite; 66 | animation-timing-function: ease; 67 | box-shadow: 0 0 20px rgba(38, 142, 199, 0); 68 | border-radius: 15px; 69 | pointer-events: none; 70 | z-index: 90000; 71 | } 72 | 73 | @keyframes pulse_animation { 74 | 0% { box-shadow: 0 0 20px rgba(38, 142, 199, 0.4); } 75 | 30% { box-shadow: 0 0 20px rgba(38, 142, 199, 1); } 76 | 60% { box-shadow: 0 0 20px rgba(38, 142, 199, 1); } 77 | 100% { box-shadow: 0 0 20px rgba(38, 142, 199, 0.4); } 78 | } 79 | -------------------------------------------------------------------------------- /src/css/admin.css: -------------------------------------------------------------------------------- 1 | .how-to .how-to-panel-content > *:first-child { 2 | margin-top: 0; 3 | } 4 | 5 | .how-to .how-to-panel-content > *:last-child { 6 | margin-bottom: 0; 7 | } 8 | 9 | .how-to { 10 | padding: 23px 10px; 11 | max-width: 800px; 12 | } 13 | 14 | .how-to-panel-content { 15 | margin: 0 13px; 16 | max-width: 800px; 17 | } 18 | -------------------------------------------------------------------------------- /src/js/admin-bar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import AdminBar from './components/AdminBar/AdminBar'; 5 | 6 | function initAdminBar(): void { 7 | const root = document.getElementById('wp-admin-bar-ab-testing-for-wp'); 8 | 9 | if (!root) return; 10 | 11 | render(, root); 12 | } 13 | 14 | document.addEventListener('DOMContentLoaded', initAdminBar); 15 | -------------------------------------------------------------------------------- /src/js/admin-editor.ts: -------------------------------------------------------------------------------- 1 | // register plugins 2 | import './plugins/ConvertButton/ConvertButton'; 3 | -------------------------------------------------------------------------------- /src/js/admin-page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import AdminPage from './components/Admin/Admin'; 5 | 6 | function onLoad(): void { 7 | const root = document.getElementById('admin_app'); 8 | 9 | if (!root) return; 10 | 11 | render(, root); 12 | } 13 | 14 | document.addEventListener('DOMContentLoaded', onLoad); 15 | -------------------------------------------------------------------------------- /src/js/blocks/ab-test-inserter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { registerBlockType, createBlock, BlockInstance } from '@wordpress/blocks'; 4 | import { __ } from '@wordpress/i18n'; 5 | import { withDispatch } from '@wordpress/data'; 6 | 7 | import SVGIcon from '../components/Logo/Logo'; 8 | import Inserter from '../components/Inserter/Inserter'; 9 | import EditWrapper from '../components/TestPreview/EditWrapper'; 10 | 11 | type EditProps = BlockInstance<{ 12 | id: string; 13 | }> & { 14 | setAttributes: (newAttributes: any) => void; 15 | removeSelf: () => void; 16 | insertNew: () => void; 17 | }; 18 | 19 | const ABTestInserter = ({ 20 | removeSelf, 21 | insertNew, 22 | attributes, 23 | setAttributes, 24 | }: EditProps): React.ReactElement => { 25 | if (attributes.id) return ; 26 | 27 | return ( 28 | setAttributes({ id: id.toString() })} 30 | removeSelf={removeSelf} 31 | insertNew={insertNew} 32 | /> 33 | ); 34 | }; 35 | 36 | const edit: any = withDispatch((dispatch, props: any) => { 37 | const { removeBlock } = dispatch('core/block-editor'); 38 | const { clientId, insertBlocksAfter } = props; 39 | 40 | const removeSelf = (): void => removeBlock(clientId); 41 | 42 | return { 43 | insertNew(): void { 44 | insertBlocksAfter(createBlock('ab-testing-for-wp/ab-test-block')); 45 | removeSelf(); 46 | }, 47 | removeSelf, 48 | }; 49 | })(ABTestInserter as any); 50 | 51 | registerBlockType('ab-testing-for-wp/ab-test-block-inserter', { 52 | title: __('A/B Test', 'ab-testing-for-wp'), 53 | description: __( 54 | 'A/B Test inserter allows you to pick an existing test or create a new one.', 55 | 'ab-testing-for-wp', 56 | ), 57 | icon: SVGIcon, 58 | category: 'widgets', 59 | attributes: { 60 | id: { 61 | type: 'string', 62 | default: '', 63 | }, 64 | }, 65 | edit, 66 | save() { 67 | return null; 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /src/js/blocks/ab-test-variant.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BlockInstance, registerBlockType, TemplateArray } from '@wordpress/blocks'; 3 | import { __, sprintf } from '@wordpress/i18n'; 4 | import { InnerBlocks } from '@wordpress/block-editor'; 5 | 6 | import SVGIcon from '../components/Logo/Logo'; 7 | 8 | import allowedBlockTypes from '../core/allowedBlockTypes'; 9 | 10 | type ABTestBlockChildProps = BlockInstance; 11 | 12 | function isValidContent(defaultContent: any): boolean { 13 | return defaultContent && defaultContent.block && defaultContent.block.name; 14 | } 15 | 16 | const edit: any = (props: ABTestBlockChildProps) => { 17 | const { attributes } = props; 18 | 19 | const { id, name, defaultContent } = attributes; 20 | 21 | const template: TemplateArray = defaultContent && isValidContent(defaultContent) 22 | ? [ 23 | [defaultContent.block.name, defaultContent.attributes || {}], 24 | ] 25 | : [ 26 | ['core/button', { 27 | text: sprintf(__('Button for Test Variant "%s"', 'ab-testing-for-wp'), name), 28 | }], 29 | ['core/paragraph', { 30 | placeholder: sprintf(__('Enter content or add blocks for test variant "%s"', 'ab-testing-for-wp'), name), 31 | }], 32 | ]; 33 | 34 | return ( 35 |
36 | 41 |
42 | ); 43 | }; 44 | 45 | const save: any = (props: ABTestBlockChildProps) => { 46 | const { attributes } = props; 47 | 48 | const { id } = attributes; 49 | 50 | return
; 51 | }; 52 | 53 | registerBlockType('ab-testing-for-wp/ab-test-block-variant', { 54 | title: __('A/B Test Variant', 'ab-testing-for-wp'), 55 | description: __('Test variant belonging to the parent A/B test container', 'ab-testing-for-wp'), 56 | icon: SVGIcon, 57 | category: 'widgets', 58 | parent: ['ab-testing-for-wp/ab-test-block'], 59 | supports: { 60 | inserter: false, 61 | reusable: false, 62 | html: false, 63 | }, 64 | attributes: { 65 | id: { 66 | type: 'string', 67 | default: '', 68 | }, 69 | name: { 70 | type: 'string', 71 | default: '', 72 | }, 73 | distribution: { 74 | type: 'number', 75 | default: 50, 76 | }, 77 | selected: { 78 | type: 'boolean', 79 | default: false, 80 | }, 81 | defaultContent: { 82 | type: 'array', 83 | default: undefined, 84 | }, 85 | }, 86 | edit, 87 | save, 88 | }); 89 | -------------------------------------------------------------------------------- /src/js/components/Admin/Admin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import queryString from 'query-string'; 4 | 5 | import Overview from './pages/Overview/Overview'; 6 | 7 | type AdminPageProps = { 8 | data?: AbTestingForWpData; 9 | reload: () => void; 10 | } 11 | 12 | function getPage(): string { 13 | const { page } = queryString.parse(window.location.search); 14 | 15 | if (!page || Array.isArray(page)) { 16 | return ''; 17 | } 18 | 19 | return page.replace('ab-testing-for-wp_', '').replace('ab-testing-for-wp', ''); 20 | } 21 | 22 | const AdminPage: React.FC = ({ data, reload }) => { 23 | if (!data) return null; 24 | 25 | const page = getPage(); 26 | 27 | switch (page) { 28 | case '': 29 | return ; 30 | default: 31 | throw new Error(`Component for ${page} can not be found`); 32 | } 33 | }; 34 | 35 | export default AdminPage; 36 | -------------------------------------------------------------------------------- /src/js/components/Admin/components/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type TableProps = { 4 | className: string; 5 | }; 6 | 7 | const Table: React.FC = ({ className, children }) => ( 8 | 9 | {children} 10 |
11 | ); 12 | 13 | export default Table; 14 | -------------------------------------------------------------------------------- /src/js/components/Admin/pages/Overview/Overview.css: -------------------------------------------------------------------------------- 1 | .ABTestVariationsTable { 2 | width: 100%; 3 | } 4 | 5 | .ABTestUplift { 6 | font-size: 0.8em; 7 | } 8 | 9 | .ABTestWinning td { 10 | color: #46B450!important; 11 | } 12 | 13 | .ABTestLosing td { 14 | color: #dc3232!important; 15 | } 16 | 17 | table.running-tests { 18 | margin-top: 2em; 19 | } 20 | 21 | .running-tests .indicator.indicator--on { 22 | background-color: #46B450; 23 | } 24 | 25 | .running-tests .indicator { 26 | background-color: #d4d4d4; 27 | width: 14px; 28 | height: 14px; 29 | border-radius: 100%; 30 | margin: 3px 0 0 0; 31 | padding: 0; 32 | border: 0; 33 | cursor: pointer; 34 | opacity: 1; 35 | } 36 | 37 | .running-tests .indicator:hover { 38 | background-color: #46B450; 39 | } 40 | 41 | .running-tests .indicator.indicator--on:hover { 42 | opacity: 0.5; 43 | } 44 | 45 | .running-tests .column-primary .row-title a { 46 | font-weight: 600; 47 | } 48 | 49 | .running-tests code { 50 | white-space: nowrap; 51 | font-size: 0.9em; 52 | } 53 | 54 | .ABTestToggle { 55 | cursor: pointer; 56 | } 57 | 58 | .ab-testing-for-wp table .check-column.check-column-normal { 59 | padding: 8px 10px; 60 | white-space: nowrap; 61 | } 62 | -------------------------------------------------------------------------------- /src/js/components/AdminBar/AdminBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, ReactNode } from 'react'; 2 | import queryString from 'query-string'; 3 | import { __, sprintf } from '@wordpress/i18n'; 4 | import apiFetch from '@wordpress/api-fetch'; 5 | 6 | import Test from './Test'; 7 | 8 | type AdminBarState = { 9 | isLoading: boolean; 10 | tests: TestData[]; 11 | pickedVariants: { 12 | [testId: string]: string; 13 | }; 14 | }; 15 | 16 | class AdminBar extends Component<{}, AdminBarState> { 17 | constructor(props: {}) { 18 | super(props); 19 | 20 | this.state = { 21 | isLoading: true, 22 | tests: [], 23 | pickedVariants: ABTestingForWP_AdminBar.participating || {}, 24 | }; 25 | } 26 | 27 | componentDidMount(): void { 28 | const testsOnPage = document.querySelectorAll('.ABTestWrapper[data-test]'); 29 | 30 | const sortedTestsOnPage = Array.from(testsOnPage).sort((a, b) => { 31 | if (a.offsetTop > b.offsetTop) { 32 | return 1; 33 | } 34 | 35 | if (a.offsetTop < b.offsetTop) { 36 | return -1; 37 | } 38 | 39 | return 0; 40 | }); 41 | 42 | const testIds = sortedTestsOnPage.map((test) => test.getAttribute('data-test')); 43 | 44 | const resolveTestData = testIds.length > 0 45 | ? apiFetch({ path: `ab-testing-for-wp/v1/get-tests-info?${queryString.stringify({ id: testIds }, { arrayFormat: 'bracket' })}` }) 46 | : Promise.resolve([]); 47 | 48 | resolveTestData.then((tests) => { 49 | this.setState({ 50 | tests: testIds 51 | .map((id) => tests.find((test) => test.id === id)) 52 | .filter((test): boolean => typeof test !== 'undefined') as TestData[], 53 | isLoading: false, 54 | }); 55 | }); 56 | } 57 | 58 | onChangeVariant = (testId: string, variantId: string): void => { 59 | const { pickedVariants } = this.state; 60 | 61 | this.setState({ 62 | pickedVariants: { 63 | ...pickedVariants, 64 | [testId]: variantId, 65 | }, 66 | }); 67 | 68 | apiFetch<{ html: string }>({ path: `ab-testing-for-wp/v1/ab-test?test=${testId}&variant=${variantId}` }) 69 | .then((result) => { 70 | if (result.html) { 71 | const target = document.querySelector(`.ABTestWrapper[data-test="${testId}"]`); 72 | 73 | if (target) target.innerHTML = result.html; 74 | } 75 | }); 76 | }; 77 | 78 | render(): ReactNode { 79 | const { isLoading, tests, pickedVariants } = this.state; 80 | 81 | return ( 82 | <> 83 |
87 | {sprintf(__('A/B Tests %s', 'ab-testing-for-wp'), tests.length > 0 ? `(${tests.length})` : '')} 88 |
89 |
90 |
    91 | {isLoading && ( 92 |
  • 93 |
    94 | {__('Scanning for tests on page', 'ab-testing-for-wp')} 95 |
    96 |
  • 97 | )} 98 | {!isLoading && tests.length === 0 99 | ? ( 100 |
  • 101 |
    102 | {__('No tests found on page', 'ab-testing-for-wp')} 103 |
    104 |
  • 105 | ) 106 | : tests.map((test) => ( 107 | 112 | ))} 113 |
114 |
115 | 116 | ); 117 | } 118 | } 119 | 120 | export default AdminBar; 121 | -------------------------------------------------------------------------------- /src/js/components/AdminBar/Test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import scrollIntoView from 'scroll-into-view'; 4 | 5 | import highlightElement from './helpers/highlight'; 6 | 7 | import Variant from './Variant'; 8 | 9 | type TestProps = { 10 | pickedVariants: { 11 | [testId: string]: string; 12 | }; 13 | onChangeVariant: (testId: string, variantId: string) => void; 14 | } & TestData; 15 | 16 | function findTestElementById(id: string): HTMLElement | null { 17 | return document.querySelector(`.ABTestWrapper[data-test="${id}"]`); 18 | } 19 | 20 | const Test: React.FC = ({ 21 | id, 22 | title, 23 | isEnabled, 24 | variants, 25 | pickedVariants, 26 | onChangeVariant, 27 | }) => { 28 | const onHover = (): void => { 29 | const element = findTestElementById(id); 30 | if (!element) return; 31 | 32 | scrollIntoView(element); 33 | highlightElement(id, element); 34 | }; 35 | 36 | return ( 37 |
  • 45 |
    46 | {title} 47 |
    48 |
    49 |
      50 | {variants.map((variant) => ( 51 | onChangeVariant(id, variantId)} 54 | isSelected={pickedVariants[id] === variant.id} 55 | /> 56 | ))} 57 |
    58 |
    59 |
  • 60 | ); 61 | }; 62 | 63 | export default Test; 64 | -------------------------------------------------------------------------------- /src/js/components/AdminBar/Variant.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | type VariantProps = { 5 | isSelected: boolean; 6 | onChangeVariant: (id: string) => void; 7 | } & TestVariant; 8 | 9 | const Variant: React.FC = ({ 10 | id, 11 | name, 12 | isSelected, 13 | onChangeVariant, 14 | }: VariantProps) => ( 15 |
  • 21 | 28 |
  • 29 | ); 30 | 31 | export default Variant; 32 | -------------------------------------------------------------------------------- /src/js/components/AdminBar/helpers/highlight.ts: -------------------------------------------------------------------------------- 1 | let lastHighlighted: string; 2 | 3 | function offsetFromRects(rect: DOMRect): { top: number; left: number } { 4 | const scrollLeft = window.pageXOffset 5 | || (document.documentElement || { scrollLeft: 0 }).scrollLeft; 6 | const scrollTop = window.pageYOffset 7 | || (document.documentElement || { scrollTop: 0 }).scrollTop; 8 | return { top: rect.top + scrollTop, left: rect.left + scrollLeft }; 9 | } 10 | 11 | export default function highlightElement(testId: string, node: HTMLElement): void { 12 | if (lastHighlighted === testId) return; 13 | 14 | lastHighlighted = testId; 15 | 16 | const highLightId = `ab-testing-for-wp__highlight__${testId}`; 17 | 18 | if (document.getElementById(highLightId)) return; 19 | 20 | document.querySelectorAll('.ab-testing-for-wp__highlight').forEach((item) => { 21 | if (!document.body) return; 22 | document.body.removeChild(item); 23 | }); 24 | 25 | const boundingRects = node.getBoundingClientRect(); 26 | const offset = offsetFromRects(boundingRects); 27 | const highlight = document.createElement('div'); 28 | const padding = 15; 29 | 30 | highlight.className = 'ab-testing-for-wp__highlight'; 31 | highlight.setAttribute('id', highLightId); 32 | 33 | highlight.style.position = 'absolute'; 34 | highlight.style.width = `${boundingRects.width + (padding * 2)}px`; 35 | highlight.style.height = `${boundingRects.height + (padding * 2)}px`; 36 | highlight.style.top = `${offset.top - padding}px`; 37 | highlight.style.left = `${offset.left - padding}px`; 38 | 39 | if (!document.body) return; 40 | 41 | document.body.appendChild(highlight); 42 | 43 | setTimeout(() => { 44 | if (!document.body || !document.body.contains(highlight)) return; 45 | document.body.removeChild(highlight); 46 | }, 5000); 47 | } 48 | -------------------------------------------------------------------------------- /src/js/components/BoxShadow/BoxShadow.css: -------------------------------------------------------------------------------- 1 | .ab-test-for-wp__BoxShadow { 2 | position: absolute; 3 | border: 1px solid #e3e5e9; 4 | right: -15px; 5 | left: -15px; 6 | top: -15px; 7 | bottom: -15px; 8 | pointer-events: none; 9 | } 10 | -------------------------------------------------------------------------------- /src/js/components/BoxShadow/BoxShadow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './BoxShadow.css'; 4 | 5 | const BoxShadow: React.FC = () =>
    ; 6 | 7 | export default BoxShadow; 8 | -------------------------------------------------------------------------------- /src/js/components/GeneralSettings/GeneralSettings.css: -------------------------------------------------------------------------------- 1 | .shortcode-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | } 6 | 7 | .shortcode-container code { 8 | font-size: 0.9em; 9 | } 10 | -------------------------------------------------------------------------------- /src/js/components/GeneralSettings/GeneralSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { __ } from '@wordpress/i18n'; 3 | import { PanelBody, ToggleControl, TextControl } from '@wordpress/components'; 4 | import { select } from '@wordpress/data'; 5 | 6 | import './GeneralSettings.css'; 7 | 8 | type GeneralSettingsProps = { 9 | isSingle: boolean; 10 | title: string; 11 | isEnabled: boolean; 12 | onChangeTitle: (title: string) => void; 13 | onChangeEnabled: (enabled: boolean) => void; 14 | }; 15 | 16 | const GeneralSettings: React.FC = ({ 17 | isSingle, 18 | title, 19 | isEnabled, 20 | onChangeTitle, 21 | onChangeEnabled, 22 | }) => { 23 | const { getCurrentPostId } = select('core/editor'); 24 | const postId = getCurrentPostId(); 25 | 26 | return ( 27 | 28 | {!isSingle && ( 29 | 34 | )} 35 | 43 | {isSingle && postId && ( 44 |
    45 | Shortcode: 46 | {`[ab-test id=${postId}]`} 47 |
    48 | )} 49 |
    50 | ); 51 | }; 52 | 53 | export default GeneralSettings; 54 | -------------------------------------------------------------------------------- /src/js/components/Inserter/Inserter.css: -------------------------------------------------------------------------------- 1 | .Inserter__actions { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | } 6 | 7 | .Inserter__loader { 8 | padding: 2em; 9 | display: flex; 10 | justify-content: center; 11 | } 12 | -------------------------------------------------------------------------------- /src/js/components/Inserter/Inserter.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { __ } from '@wordpress/i18n'; 4 | import { Modal, Button, SelectControl } from '@wordpress/components'; 5 | import apiFetch from '@wordpress/api-fetch'; 6 | 7 | import Loader from '../Loader/Loader'; 8 | 9 | import './Inserter.css'; 10 | 11 | interface WPPost { 12 | ID: string; 13 | post_title: string; 14 | } 15 | 16 | interface InserterProps { 17 | pickTest: (id: string) => void; 18 | removeSelf: () => void; 19 | insertNew: () => void; 20 | } 21 | 22 | interface InserterState { 23 | value: string; 24 | isLoading: boolean; 25 | isPicking: boolean; 26 | options: WPPost[]; 27 | } 28 | 29 | class Inserter extends Component { 30 | constructor(props: InserterProps) { 31 | super(props); 32 | 33 | this.state = { 34 | value: '', 35 | isLoading: true, 36 | isPicking: false, 37 | options: [], 38 | }; 39 | } 40 | 41 | componentDidMount(): void { 42 | apiFetch({ path: 'ab-testing-for-wp/v1/get-posts-by-type?type=abt4wp-test' }) 43 | .then((options) => { 44 | if (options.length === 0) { 45 | this.insertNew(); 46 | return; 47 | } 48 | 49 | this.setState({ 50 | isLoading: false, 51 | value: options[0].ID, 52 | options, 53 | }); 54 | }); 55 | } 56 | 57 | insertNew = (): void => { 58 | const { insertNew } = this.props; 59 | 60 | insertNew(); 61 | }; 62 | 63 | cancelInsert = (): void => { 64 | const { removeSelf } = this.props; 65 | 66 | removeSelf(); 67 | }; 68 | 69 | insertExisting = (): void => { 70 | const { pickTest } = this.props; 71 | const { value } = this.state; 72 | 73 | pickTest(value); 74 | }; 75 | 76 | render(): React.ReactElement { 77 | const { 78 | value, 79 | isLoading, 80 | isPicking, 81 | options, 82 | } = this.state; 83 | 84 | if (isLoading) return
    ; 85 | 86 | return ( 87 | 92 | {isPicking ? ( 93 |
    94 | ({ 98 | label: option.post_title, 99 | value: option.ID, 100 | }))} 101 | onChange={(newValue): void => this.setState({ value: newValue })} 102 | /> 103 | 106 | 109 |
    110 | ) : ( 111 |
    112 | 115 | 118 |
    119 | )} 120 |
    121 | ); 122 | } 123 | } 124 | 125 | export default Inserter; 126 | -------------------------------------------------------------------------------- /src/js/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loader: React.FC = () => ( 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | ); 30 | 31 | export default Loader; 32 | -------------------------------------------------------------------------------- /src/js/components/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Logo: React.FC = () => ( 4 | 12 | 13 | 14 | 20 | 24 | 28 | 29 | ); 30 | 31 | export default Logo; 32 | -------------------------------------------------------------------------------- /src/js/components/Onboarding/Arrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Arrow: React.FC = (props) => ( 4 | 12 | 21 | 29 | 30 | ); 31 | 32 | export default Arrow; 33 | -------------------------------------------------------------------------------- /src/js/components/Onboarding/Onboarding.css: -------------------------------------------------------------------------------- 1 | .ab-testing-for-wp__Onboarding .content p { 2 | font-size: 1.2em; 3 | } 4 | 5 | .ab-testing-for-wp__Onboarding .content { 6 | padding: 0 15px; 7 | max-width: 320px; 8 | text-align: center; 9 | } 10 | 11 | .ab-testing-for-wp__Onboarding .buttons { 12 | display: flex; 13 | align-items: stretch; 14 | justify-items: stretch; 15 | border-top: 1px solid #ededed; 16 | } 17 | 18 | .ab-testing-for-wp__Onboarding .buttons button.next { 19 | background: #ededed; 20 | } 21 | 22 | .ab-testing-for-wp__Onboarding .buttons button { 23 | border: 0; 24 | flex-grow: 1; 25 | padding: 12px 0; 26 | background: #fff; 27 | font-size: 1.2em; 28 | transition: background 0.2s ease; 29 | cursor: pointer; 30 | } 31 | 32 | .ab-testing-for-wp__Onboarding .buttons button:focus, 33 | .ab-testing-for-wp__Onboarding .buttons button:hover { 34 | color: #fff; 35 | background: #007eb1; 36 | } 37 | 38 | .ab-testing-for-wp__Onboarding .buttons button:hover { 39 | background: #0075a4; 40 | } 41 | 42 | .ab-testing-for-wp__Onboarding { 43 | position: absolute; 44 | z-index: 999999; 45 | background: #fff; 46 | box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.5); 47 | display: none; 48 | } 49 | 50 | .ab-testing-for-wp__OnboardingOverlay { 51 | position: fixed; 52 | z-index: 999998; 53 | top: 0; 54 | left: 0; 55 | width: 100%; 56 | height: 100%; 57 | background-color: rgba(0, 0, 0, 0.5); 58 | transition: clip-path 0.3s ease; 59 | } 60 | 61 | #OverboardingPreventClick { 62 | position: fixed; 63 | z-index: 999998; 64 | top: 0; 65 | left: 0; 66 | width: 100%; 67 | height: 100%; 68 | } 69 | 70 | .ab-testing-for-wp__OnboardingModal .ButtonContainer { 71 | margin: 2em 0 1em 0; 72 | display: flex; 73 | align-items: center; 74 | } 75 | 76 | .ab-testing-for-wp__OnboardingModal .ButtonContainer button { 77 | margin-right: 10px; 78 | } 79 | -------------------------------------------------------------------------------- /src/js/components/Onboarding/Overlay.tsx: -------------------------------------------------------------------------------- 1 | export function drawOverlayAround( 2 | target: HTMLElement, 3 | spacingTop = 0, 4 | spacingRight = 0, 5 | spacingBottom = 0, 6 | spacingLeft = 0, 7 | ): void { 8 | let overlay = document.getElementById('OverboardingOverlay'); 9 | let preventClick = document.getElementById('OverboardingPreventClick'); 10 | 11 | if (!overlay) { 12 | overlay = document.createElement('div'); 13 | overlay.setAttribute('id', 'OverboardingOverlay'); 14 | overlay.setAttribute('class', 'ab-testing-for-wp__OnboardingOverlay'); 15 | 16 | if (!document.body) return; 17 | document.body.appendChild(overlay); 18 | } 19 | 20 | if (!preventClick) { 21 | preventClick = document.createElement('div'); 22 | preventClick.setAttribute('id', 'OverboardingPreventClick'); 23 | 24 | if (!document.body) return; 25 | document.body.appendChild(preventClick); 26 | } 27 | 28 | const boundingRects = target.getBoundingClientRect(); 29 | 30 | const top = boundingRects.top - spacingTop; 31 | const left = boundingRects.left - spacingLeft; 32 | const right = boundingRects.right + spacingRight; 33 | const bottom = boundingRects.bottom + spacingBottom; 34 | 35 | overlay.style.clipPath = `polygon(0 0, 0 100%, ${left}px 100%, ${left}px ${top}px, ${right}px ${top}px, ${right}px ${bottom}px, ${left}px ${bottom}px, ${left}px 100%, 100% 100%, 100% 0%)`; 36 | } 37 | 38 | export function removeOverlay(): void { 39 | const overlay = document.getElementById('OverboardingOverlay'); 40 | if (!overlay) return; 41 | const parent = overlay.parentNode; 42 | if (!parent) return; 43 | parent.removeChild(overlay); 44 | 45 | const preventClick = document.getElementById('OverboardingPreventClick'); 46 | if (!preventClick) return; 47 | const preventClickParent = preventClick.parentNode; 48 | if (!preventClickParent) return; 49 | preventClickParent.removeChild(preventClick); 50 | } 51 | -------------------------------------------------------------------------------- /src/js/components/Significance/Significance.css: -------------------------------------------------------------------------------- 1 | .Significance { 2 | border-left: 4px solid #aaa; 3 | background-color: #eee; 4 | padding: 0.5em 1em; 5 | } 6 | 7 | .Significance.Significance--success { 8 | border-left-color: #46B450; 9 | background-color: #e3efe3; 10 | } 11 | 12 | .Significance.Significance--failed { 13 | border-left-color: #dc3232; 14 | background-color: #f5ebec; 15 | } 16 | -------------------------------------------------------------------------------- /src/js/components/Significance/Significance.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { __, sprintf } from '@wordpress/i18n'; 3 | 4 | import classNames from 'classnames'; 5 | 6 | import calcTestWinner, { TestWinner } from '../../helpers/calcTestWinner'; 7 | 8 | import './Significance.css'; 9 | 10 | function getTranslationString(control: string, testResult: TestWinner): string { 11 | if (!testResult.confident) { 12 | return 'Test results for are NOT significant enough to declare a winner yet.'; 13 | } 14 | 15 | return testResult.winner.id !== control 16 | ? 'Test results are significant enough to declare variation %s the winner with 95%% confidence.' 17 | : 'Test results are significant enough to say control variation %s remains a winner with 95%% confidence.'; 18 | } 19 | 20 | type SignificanceProps = { 21 | control: string; 22 | results: ABTestResult[]; 23 | }; 24 | 25 | const Significance: React.FC = ({ control, results }) => { 26 | const testResult = calcTestWinner(control, results); 27 | 28 | if (!testResult) return null; 29 | 30 | return ( 31 |
    40 | {sprintf( 41 | __(getTranslationString(control, testResult), 'ab-testing-for-wp'), 42 | testResult.winner.name, 43 | )} 44 |
    45 | ); 46 | }; 47 | 48 | export default Significance; 49 | -------------------------------------------------------------------------------- /src/js/components/TestPreview/EditWrapper.css: -------------------------------------------------------------------------------- 1 | .EditWrapper { 2 | position: relative; 3 | } 4 | 5 | .EditWrapper__Overlay { 6 | position: absolute; 7 | background: rgba(255, 255, 255, 0.7); 8 | width: 100%; 9 | height: 100%; 10 | opacity: 0; 11 | transition: opacity 0.2s ease; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .EditWrapper__Overlay:hover { 18 | opacity: 1; 19 | } 20 | -------------------------------------------------------------------------------- /src/js/components/TestPreview/EditWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import { Button } from '@wordpress/components'; 4 | import { __ } from '@wordpress/i18n'; 5 | import apiFetch from '@wordpress/api-fetch'; 6 | 7 | import TestPreview from './TestPreview'; 8 | import Loader from '../Loader/Loader'; 9 | 10 | import { decodeLink } from '../../helpers/wordpress'; 11 | 12 | import './EditWrapper.css'; 13 | 14 | interface EditWrapperProps { 15 | id: string; 16 | } 17 | 18 | interface EditWrapperState { 19 | isLoading: boolean; 20 | html: string; 21 | editLink: string; 22 | } 23 | 24 | class EditWrapper extends Component { 25 | constructor(props: EditWrapperProps) { 26 | super(props); 27 | 28 | this.state = { 29 | isLoading: true, 30 | html: '', 31 | editLink: '', 32 | }; 33 | } 34 | 35 | componentDidMount(): void { 36 | const { id } = this.props; 37 | 38 | apiFetch<{ html: string; editLink: string }>({ path: `ab-testing-for-wp/v1/get-test-content-by-post?id=${id}` }) 39 | .then((result) => { 40 | this.setState({ 41 | isLoading: false, 42 | html: result.html, 43 | editLink: decodeLink(result.editLink), 44 | }); 45 | }); 46 | } 47 | 48 | render(): React.ReactElement { 49 | const { isLoading, html, editLink } = this.state; 50 | 51 | return ( 52 |
    53 |
    54 | 57 |
    58 | {isLoading && !html ? : } 59 |
    60 | ); 61 | } 62 | } 63 | 64 | export default EditWrapper; 65 | -------------------------------------------------------------------------------- /src/js/components/TestPreview/TestPreview.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import shortid from 'shortid'; 3 | 4 | type TestPreviewProps = { 5 | html: string; 6 | }; 7 | 8 | class TestPreview extends Component { 9 | iframeRef = createRef(); 10 | 11 | tempId: string = shortid.generate(); 12 | 13 | componentDidMount(): void { 14 | const { html } = this.props; 15 | 16 | window.addEventListener('message', this.listenToHeight); 17 | 18 | if (!this.iframeRef.current) return; 19 | this.iframeRef.current.setAttribute( 20 | 'src', 21 | `data:text/html;charset=utf-8,${escape(html.replace('%ab-testing-id%', this.tempId))}`, 22 | ); 23 | } 24 | 25 | componentWillUnmount(): void { 26 | window.removeEventListener('message', this.listenToHeight); 27 | } 28 | 29 | listenToHeight = (e: MessageEvent): void => { 30 | try { 31 | if (typeof e.data !== 'string') return; 32 | const data = JSON.parse(e.data); 33 | if (data.from !== this.tempId) return; 34 | if (!this.iframeRef.current) return; 35 | this.iframeRef.current.style.height = `${data.height}px`; 36 | } catch (err) { 37 | // fail silently 38 | } 39 | }; 40 | 41 | render(): React.ReactElement { 42 | return