├── .eslintrc.js ├── .gitignore ├── README.md ├── e2e ├── bootstrap.js └── extension.spec.js ├── extension ├── background │ ├── background.js │ └── templates.js ├── content_scripts │ ├── common.js │ └── toggleListeners.js ├── logo │ ├── JesteerLogo_128px.png │ ├── JesteerLogo_16px.png │ ├── JesteerLogo_32px.png │ ├── JesteerLogo_48px.png │ └── JesteerLogo_64px.png ├── manifest.json └── popup │ ├── popup.html │ ├── popup.js │ ├── recording.js │ ├── snapshot.js │ └── style.css ├── jest.config.js ├── package-lock.json ├── package.json └── splash ├── .gitignore ├── package-lock.json ├── package.json ├── public ├── build │ ├── bundle.css │ ├── bundle.js │ └── bundle.js.map ├── favicon.png ├── global.css ├── img │ ├── 640px.gif │ ├── GitHubLogo.png │ ├── JesteerLogo.svg │ ├── JesteerLogo2.png │ ├── LinkedInLogo.png │ ├── cha.jpeg │ ├── clare.jpeg │ ├── dark-denim-3.png │ ├── katie.png │ ├── noise-lines.png │ ├── noise.png │ └── tim.jpeg └── index.html ├── rollup.config.js └── src ├── App.svelte ├── Components ├── Footer.svelte ├── HowTo.svelte ├── Intro.svelte ├── Nav.svelte └── Team.svelte └── main.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | sourceType: 'module', 13 | }, 14 | globals: { 15 | chrome: false, 16 | getSelectorPath: false, 17 | }, 18 | rules: { 19 | 'import/extensions': ['error', 'always', { ignorePackages: true }], 20 | 'brace-style': ['error', 'stroustrup'], 21 | 'import/prefer-default-export': 'off', 22 | 'no-plusplus': 'off', 23 | 'no-restricted-syntax': 'off', 24 | 'no-underscore-dangle': 'off', 25 | 'one-var': 'off', 26 | 'one-var-declaration-per-line': 'off', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | extension.pem 4 | *.zip 5 | *.crx 6 | coverage 7 | .nyc_output 8 | generated_tests -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jesteer 2 | Jesteer is a Chrome extension that records your browser interactions and generates Puppeteer script. 3 |

4 | Jesteer logo 7 |

8 | 9 | ## Overview 10 | With Jesteer, you can automatically generate E2E tests simply by navigating through your application. 11 | - Testing becomes faster, easier, and less error-prone 12 | - Developers can spend less time testing, and even non-coders can write robust E2E tests. 13 | - Conquer seemingly-insurmountable test debt 14 | - Test apps like end-consumers **use** aps: via the application's UI. 15 | 16 | Accelarated by OS Labs. 17 | 18 | 19 |

20 | Jesteer logo 23 |

24 | 25 | ## Main features 26 | - Create end-to-end tests using Puppeteer and Jest, without writing a single line of code 27 | - Record browser interactions 28 | - Most basic click and type interactions are supported, including navigation 29 | - Generate Puppeteer script 30 | - Take snapshots of DOM elements for regression testing 31 | - Simple UI makes writing tests a pleasure 32 | 33 | ## How to use it? 34 | - Install Jesteer from the Chrome Web 35 | - Go to the webpage you want to start at for your E2E test 36 | - Open Jesteer, press 'Start Recording' 37 | - Navigate a path through your app: Jesteer is recording. 38 | - When you've finished navigating, open up Jesteer again, click 'Take snapshot' and then take a snapshot of the part of your page displaying the expected behavior 39 | - Optionally continue navigating, take more snapshots 40 | - When finished, click 'Stop Recording', and the input box will be populated with your newly generated test 41 | - Copy/paste into your test suite 42 | 43 | ## Contributors 44 | Timothy Ruszala 45 | 46 | Katie Janzen 47 | 48 | Clare Cerullo 49 | 50 | Charissa D. Ramirez 51 | 52 | ## Acknowledgments 53 | A big thank you to the tech accelerator Open Source Labs for their continued support and sponsorship throughout this whole process. 54 | 55 | This project is licensed under the ISC license. 56 | -------------------------------------------------------------------------------- /e2e/bootstrap.js: -------------------------------------------------------------------------------- 1 | // file adapted from: https://tweak-extension.com/blog/complete-guide-test-chrome-extension-puppeteer/ 2 | const puppeteer = require('puppeteer'); 3 | 4 | // launches browser to test our chrome extension, and gives us access to variables 5 | // which reference the browser, the app page, the extension page, and the extension url 6 | async function bootstrap(options = {}) { 7 | const { devtools = false, slowMo = false, appUrl } = options; 8 | const browser = await puppeteer.launch({ 9 | headless: false, 10 | devtools, 11 | args: [ 12 | '--disable-extensions-except=./extension', 13 | '--load-extension=./extension', 14 | '--user-agent=PuppeteerAgent', 15 | ], 16 | ...(slowMo && { slowMo }), 17 | }); 18 | 19 | const appPage = await browser.newPage(); 20 | // const pages = await browser.pages(); 21 | // const appPage = pages[0]; 22 | await appPage.goto(appUrl, { waitUntil: 'load' }); 23 | 24 | const targets = await browser.targets(); 25 | const extensionTarget = targets.find( 26 | (target) => target.type() === 'service_worker', 27 | ); 28 | const partialExtensionUrl = extensionTarget._targetInfo.url || ''; 29 | const [, , extensionId] = partialExtensionUrl.split('/'); 30 | 31 | const extPage = await browser.newPage(); 32 | const extensionUrl = `chrome-extension://${extensionId}/popup/popup.html`; 33 | await extPage.goto(extensionUrl, { waitUntil: 'load' }); 34 | 35 | return { 36 | appPage, 37 | browser, 38 | extensionUrl, 39 | extPage, 40 | }; 41 | } 42 | 43 | module.exports = { bootstrap }; 44 | -------------------------------------------------------------------------------- /e2e/extension.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | When you test, be sure to add tabs permissions to manifest.json. 3 | For production, we only need activeTab, so the published extension 4 | does not ask for tabs permission. The testing set-up, however, *does* 5 | need tabs permission. 6 | */ 7 | 8 | // const pti = require('puppeteer-to-istanbul'); 9 | const { bootstrap } = require('./bootstrap.js'); 10 | 11 | describe('Popup Functionality', () => { 12 | let extPage, appPage, browser; 13 | 14 | // beforeAll(async () => { 15 | // // const context = await bootstrap({ 16 | // // appUrl: 'https://www.wikipedia.org/', 17 | // // slowMo: 150, 18 | // // devtools: true, 19 | // // }); 20 | 21 | // // extPage = context.extPage; 22 | // // appPage = context.appPage; 23 | // // browser = context.browser; 24 | 25 | // // await Promise.all([ 26 | // // extPage.coverage.startJSCoverage(), 27 | // // extPage.coverage.startCSSCoverage(), 28 | // // ]); 29 | // }); 30 | 31 | beforeEach(async () => { 32 | const context = await bootstrap({ 33 | appUrl: 'https://www.wikipedia.org/', 34 | // make sure to have some slowMo to avoid a race-condition, causing tests to unexpectedly fail 35 | slowMo: 150, 36 | devtools: true, 37 | }); 38 | 39 | extPage = context.extPage; 40 | appPage = context.appPage; 41 | browser = context.browser; 42 | }); 43 | 44 | afterEach(async () => { 45 | await browser.close(); 46 | }); 47 | 48 | // afterAll(async () => { 49 | // // measure code coverage with istanbul 50 | // const [jsCoverage, cssCoverage] = await Promise.all([ 51 | // extPage.coverage.stopJSCoverage(), 52 | // extPage.coverage.stopCSSCoverage(), 53 | // ]); 54 | // let totalBytes = 0; 55 | // let usedBytes = 0; 56 | // const coverage = [...jsCoverage, ...cssCoverage]; 57 | 58 | // for (const entry of coverage) { 59 | // totalBytes += entry.text.length; 60 | // for (const range of entry.ranges) { 61 | // usedBytes += range.end - range.start - 1; 62 | // } 63 | // } 64 | 65 | // // eslint-disable-next-line 66 | // console.log(`Bytes used: ${(usedBytes / totalBytes) * 100}%`); 67 | 68 | // pti.write([...jsCoverage, ...cssCoverage], { 69 | // includeHostname: true, 70 | // storagePath: './.nyc_output', 71 | // }); 72 | 73 | // }); 74 | 75 | it('should click on the search box, i.e. wikipedia should load', async () => { 76 | appPage.bringToFront(); 77 | const searchBox = await appPage.$('#searchInput'); 78 | await searchBox.click(); 79 | }); 80 | 81 | it('should record and then stop recording', async () => { 82 | extPage.bringToFront(); 83 | const btnRecord = await extPage.$('#btnRecord'); 84 | await extPage.waitForFunction( 85 | 'document.querySelector(\'#btnRecord\').innerText === \'Record\'', 86 | ); 87 | const btnRecordText = await btnRecord.evaluate((e) => e.innerText); 88 | expect(btnRecordText).toBe('Record'); 89 | 90 | await btnRecord.click(); 91 | expect( 92 | await extPage.waitForFunction( 93 | 'document.querySelector(\'#btnRecord\').innerText === \'Stop Recording\'', 94 | ), 95 | ).toBeTruthy(); 96 | }); 97 | 98 | it('should record a very simple browser interaction', async () => { 99 | await extPage.bringToFront(); 100 | await extPage.waitForSelector('#btnRecord'); 101 | let btnRecord = await extPage.$('#btnRecord'); 102 | await btnRecord.click(); 103 | 104 | await appPage.bringToFront(); 105 | await appPage.waitForSelector('#searchInput'); 106 | const searchBox = await appPage.$('#searchInput'); 107 | await searchBox.click(); 108 | 109 | await extPage.bringToFront(); 110 | await extPage.waitForSelector('#btnRecord'); 111 | btnRecord = await extPage.$('#btnRecord'); 112 | await btnRecord.click(); 113 | await extPage.waitForSelector('#codegen'); 114 | const output = await extPage.$eval('#codegen', (e) => e.value); 115 | const expectedOutput = `/* 116 | This test suite was created using JESTEER, a project developed by 117 | Tim Ruszala, Katie Janzen, Clare Cerullo, and Charissa Ramirez. 118 | 119 | Learn more at https://github.com/oslabs-beta/Jesteer . 120 | */ 121 | const puppeteer = require('puppeteer'); // v13.0.0 or later 122 | 123 | jest.setTimeout(10000); 124 | describe('', () => { 125 | 126 | let browser, page, timeout; 127 | 128 | beforeAll(async () => { 129 | browser = await puppeteer.launch({ 130 | headless: true, 131 | }); 132 | }); 133 | 134 | beforeEach(async () => { 135 | page = await browser.newPage(); 136 | timeout = 5000; 137 | page.setDefaultTimeout(timeout); 138 | }); 139 | 140 | afterEach(async () => { 141 | await page.close(); 142 | }); 143 | 144 | afterAll(async () => { 145 | await browser.close(); 146 | }); 147 | 148 | it('', async () => { 149 | 150 | { 151 | const promises = []; 152 | promises.push(page.waitForNavigation()); 153 | await page.goto('https://www.wikipedia.org/'); 154 | await Promise.all(promises); 155 | } 156 | 157 | { 158 | const element = await page.waitForSelector('#searchInput'); 159 | await element.click(); 160 | } 161 | 162 | }); 163 | 164 | }); 165 | `; 166 | expect(output).toBe(expectedOutput); 167 | }); 168 | 169 | it('should take a snapshot of the selected element', async () => { 170 | await extPage.bringToFront(); 171 | await extPage.waitForSelector('#btnRecord'); 172 | let btnRecord = await extPage.$('#btnRecord'); 173 | await btnRecord.click(); 174 | 175 | await extPage.waitForSelector('#btnSnapshot'); 176 | const btnSnapshot = await extPage.$('#btnSnapshot'); 177 | await btnSnapshot.click(); 178 | 179 | await appPage.bringToFront(); 180 | await appPage.waitForSelector('.footer-sidebar-text'); 181 | const footer = await appPage.$('.footer-sidebar-text'); 182 | await footer.click(); 183 | 184 | await extPage.bringToFront(); 185 | await extPage.waitForSelector('#btnRecord'); 186 | btnRecord = await extPage.$('#btnRecord'); 187 | await btnRecord.click(); 188 | await extPage.waitForSelector('#codegen'); 189 | const output = await extPage.$eval('#codegen', (e) => e.value); 190 | const expectedOutput = `/* 191 | This test suite was created using JESTEER, a project developed by 192 | Tim Ruszala, Katie Janzen, Clare Cerullo, and Charissa Ramirez. 193 | 194 | Learn more at https://github.com/oslabs-beta/Jesteer . 195 | */ 196 | const puppeteer = require('puppeteer'); // v13.0.0 or later 197 | 198 | jest.setTimeout(10000); 199 | describe('', () => { 200 | 201 | let browser, page, timeout; 202 | 203 | beforeAll(async () => { 204 | browser = await puppeteer.launch({ 205 | headless: true, 206 | }); 207 | }); 208 | 209 | beforeEach(async () => { 210 | page = await browser.newPage(); 211 | timeout = 5000; 212 | page.setDefaultTimeout(timeout); 213 | }); 214 | 215 | afterEach(async () => { 216 | await page.close(); 217 | }); 218 | 219 | afterAll(async () => { 220 | await browser.close(); 221 | }); 222 | 223 | it('', async () => { 224 | 225 | { 226 | const promises = []; 227 | promises.push(page.waitForNavigation()); 228 | await page.goto('https://www.wikipedia.org/'); 229 | await Promise.all(promises); 230 | } 231 | 232 | { 233 | const snapped = await page.$eval('#www-wikipedia-org > DIV:nth-child(8) > DIV:nth-child(1) > DIV:nth-child(1) > DIV:nth-child(2)', el => el.innerHTML); 234 | expect(snapped).toMatchSnapshot(); 235 | } 236 | 237 | }); 238 | 239 | }); 240 | `; 241 | expect(output).toBe(expectedOutput); 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /extension/background/background.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: off */ 2 | 3 | import * as templates from './templates.js'; 4 | import { toggleListeners } from '../content_scripts/toggleListeners.js'; 5 | 6 | // current actions acts as a queue containing the actions performed on the page while recording, 7 | // including clicks and snapshots. 8 | let actions = []; 9 | let keysPressed = ''; 10 | 11 | function flushKeyBuffer() { 12 | if (keysPressed) { 13 | actions.push({ type: 'keyboard', text: keysPressed }); 14 | keysPressed = ''; 15 | } 16 | } 17 | 18 | function handleRecordAction(action) { 19 | // adds a type action to the actions queue, with whatever had last been typed 20 | flushKeyBuffer(); 21 | 22 | // bug-fix: when we take a snapshot, both a 'click' and a 'snapshot' action get registered. 23 | // We need to make sure only the snapshot action is registered, so we pop off the click. 24 | if (action.type === 'snapshot') actions.pop(); 25 | 26 | // Push the action object from the message into the actions array 27 | actions.push(action); 28 | } 29 | 30 | // When we stop recording, we go through all actions in the actions queue and use them to 31 | // build out the test suite. 32 | function processActionsQueue() { 33 | let outputString = templates.testSuiteStart 34 | + templates.describeStart 35 | + templates.itBlockStart; 36 | 37 | if (actions[0].type !== 'initialURL') { 38 | // Handle the occasional edge case where a recording fails to start correctly 39 | // Construct an initialURL action object and put it at the front of the actions queue 40 | // This will write a comment asking the tester to replace it with the Initial Page URL 41 | // This is a better way to fail than not generating a test at all 42 | actions.unshift({ 43 | type: 'initialURL', 44 | url: '/* This URL failed to generate as a part of the recording process. Please replace this comment with the Initial Page URL. */', 45 | }); 46 | } 47 | 48 | for (const action of actions) { 49 | switch (action.type) { 50 | case 'initialURL': 51 | outputString += templates.gotoInitialPage(action.url); 52 | break; 53 | 54 | case 'keyboard': 55 | outputString += templates.keyboard(action.text); 56 | break; 57 | 58 | case 'keyboardPress': 59 | outputString += templates.keyboardPress(action.key); 60 | break; 61 | 62 | case 'click': 63 | outputString += templates.click(action.element); 64 | break; 65 | 66 | case 'navigation': 67 | outputString += templates.waitForNav; 68 | break; 69 | 70 | case 'snapshot': 71 | outputString += templates.snapshot(action.element); 72 | break; 73 | 74 | default: 75 | break; 76 | } 77 | } 78 | 79 | outputString += templates.blockEndMultiple(2); 80 | 81 | actions = []; 82 | 83 | return outputString; 84 | } 85 | 86 | // initializes chrome storage on setup 87 | // Initialize our state to reflect that we are not yet recording on extension startup 88 | chrome.runtime.onStartup.addListener(() => { 89 | // Set a value in the extension local storage 90 | chrome.storage.local.set({ recording: false, currentTest: '' }); 91 | }); 92 | 93 | // Check for page navigation 94 | chrome.tabs.onUpdated.addListener((tabId, changeInfo /* , tab */) => { 95 | // read changeInfo data to see if url changed 96 | if (changeInfo.url) { 97 | chrome.storage.local.get('recording', async ({ recording }) => { 98 | if (recording) { 99 | const navigationAction = { type: 'navigation' }; 100 | handleRecordAction(navigationAction); 101 | 102 | // Insert code for functions shared across popup 103 | chrome.scripting.executeScript({ 104 | target: { tabId }, 105 | files: ['content_scripts/common.js'], 106 | }); 107 | 108 | // turn off existing event listeners 109 | chrome.scripting.executeScript({ 110 | target: { tabId }, 111 | function: toggleListeners, 112 | args: [false], 113 | }); 114 | 115 | // turn event listeners back on (fresh start) 116 | chrome.scripting.executeScript({ 117 | target: { tabId }, 118 | function: toggleListeners, 119 | args: [true], 120 | }); 121 | } 122 | }); 123 | } 124 | }); 125 | 126 | // Listen for messages sent from elsewhere across the extension 127 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 128 | // handle messages based on their type 129 | switch (message.type) { 130 | // handle a keypress 131 | case 'keydown': 132 | if (message.key.length > 1) { 133 | handleRecordAction({ type: 'keyboardPress', key: message.key }); 134 | } 135 | else if (message.key === '\\') { 136 | keysPressed += '\\\\'; 137 | } 138 | else { 139 | keysPressed += message.key; 140 | } 141 | sendResponse({ ok: true }); 142 | break; 143 | 144 | // when the user interacts with the webpage, whatever they interact with 145 | // is emitted as a 'recordAction' message 146 | case 'recordAction': 147 | handleRecordAction(message.action); 148 | sendResponse({ ok: true }); 149 | break; 150 | 151 | // user clicks the 'stop recording button' 152 | case 'stopRecording': 153 | { 154 | // Compile the final file for output 155 | flushKeyBuffer(); 156 | const outputString = processActionsQueue(); 157 | sendResponse({ ok: true, output: outputString }); 158 | } 159 | break; 160 | 161 | // Log something to the Service Worker Console 162 | case 'log': 163 | console.log(message.text); 164 | sendResponse({ ok: true }); 165 | break; 166 | 167 | // Received unknown message 168 | default: 169 | console.log('Received Unknown Message'); 170 | sendResponse({ ok: false }); 171 | break; 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /extension/background/templates.js: -------------------------------------------------------------------------------- 1 | export const testSuiteStart = `/* 2 | This test suite was created using JESTEER, a project developed by 3 | Tim Ruszala, Katie Janzen, Clare Cerullo, and Charissa Ramirez. 4 | 5 | Learn more at https://github.com/oslabs-beta/Jesteer . 6 | */ 7 | const puppeteer = require('puppeteer'); // v13.0.0 or later 8 | `; 9 | 10 | export const describeStart = ` 11 | jest.setTimeout(10000); 12 | describe('', () => { 13 | 14 | let browser, page, timeout; 15 | 16 | beforeAll(async () => { 17 | browser = await puppeteer.launch({ 18 | headless: true, 19 | }); 20 | }); 21 | 22 | beforeEach(async () => { 23 | page = await browser.newPage(); 24 | timeout = 5000; 25 | page.setDefaultTimeout(timeout); 26 | }); 27 | 28 | afterEach(async () => { 29 | await page.close(); 30 | }); 31 | 32 | afterAll(async () => { 33 | await browser.close(); 34 | }); 35 | `; 36 | 37 | export const itBlockStart = ` 38 | it('', async () => { 39 | `; 40 | 41 | export const blockEnd = ` 42 | }); 43 | `; 44 | 45 | export const waitForNav = ` 46 | await page.waitForNavigation(); 47 | `; 48 | 49 | export const pressEnter = ` 50 | await page.keyboard.press('Enter'); 51 | `; 52 | 53 | export const blockEndMultiple = (count) => blockEnd.repeat(count); 54 | 55 | export const gotoInitialPage = (initialPageURL) => (` 56 | { 57 | const promises = []; 58 | promises.push(page.waitForNavigation()); 59 | await page.goto('${initialPageURL}'); 60 | await Promise.all(promises); 61 | } 62 | `); 63 | 64 | export const keyboard = (text) => (` 65 | await page.keyboard.type('${text}'); 66 | `); 67 | 68 | export const keyboardPress = (key) => (` 69 | await page.keyboard.press('${key}'); 70 | `); 71 | 72 | export const click = (selector) => (` 73 | { 74 | const element = await page.waitForSelector('${selector}'); 75 | await element.click(); 76 | } 77 | `); 78 | 79 | export const snapshot = (selector) => (` 80 | { 81 | const snapped = await page.$eval('${selector}', el => el.innerHTML); 82 | expect(snapped).toMatchSnapshot(); 83 | } 84 | `); 85 | -------------------------------------------------------------------------------- /extension/content_scripts/common.js: -------------------------------------------------------------------------------- 1 | // common.js stores functions that are used by other injected content scripts. 2 | 3 | // Return a Selector Path to the given element. 4 | // eslint-disable-next-line no-unused-vars 5 | function getSelectorPath(element) { 6 | const names = []; 7 | let current = element; 8 | while (current.parentNode) { 9 | if (current.id) { 10 | names.unshift(`#${current.id}`); 11 | break; 12 | } 13 | else if (current === current.ownerDocument.documentElement) { 14 | names.unshift(current.tagName); 15 | } 16 | else { 17 | let e = current; 18 | let i = 1; 19 | 20 | while (e.previousElementSibling) { 21 | e = e.previousElementSibling; 22 | i++; 23 | } 24 | names.unshift(`${current.tagName}:nth-child(${i})`); 25 | } 26 | current = current.parentNode; 27 | } 28 | return names.join(' > '); 29 | } 30 | -------------------------------------------------------------------------------- /extension/content_scripts/toggleListeners.js: -------------------------------------------------------------------------------- 1 | // Enables and Disables the Event Listeners that monitor the webpage 2 | export function toggleListeners(rec) { 3 | // When an element in the current webpage is clicked, send a message back to the chrome extension 4 | // From there in can be added to the recorded actions 5 | const handleClick = (e) => { 6 | const recordActionMessage = { 7 | type: 'recordAction', 8 | action: { 9 | type: 'click', 10 | element: getSelectorPath(e.target), 11 | }, 12 | }; 13 | 14 | chrome.runtime.sendMessage(recordActionMessage); 15 | }; 16 | 17 | const handleKeydown = (e) => { 18 | const { key } = e; 19 | chrome.runtime.sendMessage({ type: 'keydown', key }); 20 | }; 21 | 22 | // start recording 23 | if (rec) { 24 | // When we begin recording, inject our event listener(s) 25 | document.___jesteer = {}; // Container object which holds functionality we injected 26 | document.___jesteer.handleClick = (e) => handleClick(e); 27 | document.___jesteer.handleKeydown = (e) => handleKeydown(e); 28 | document.addEventListener('click', document.___jesteer.handleClick); 29 | document.addEventListener('keydown', document.___jesteer.handleKeydown); 30 | } 31 | // stop recording, remove event listeners 32 | else if (document.___jesteer) { 33 | document.removeEventListener('click', document.___jesteer.handleClick); 34 | document.removeEventListener('keydown', document.___jesteer.handleKeydown); 35 | } 36 | // chrome.runtime.sendMessage({ type: 'stopRecording', url: window.location.href }); 37 | } 38 | -------------------------------------------------------------------------------- /extension/logo/JesteerLogo_128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/extension/logo/JesteerLogo_128px.png -------------------------------------------------------------------------------- /extension/logo/JesteerLogo_16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/extension/logo/JesteerLogo_16px.png -------------------------------------------------------------------------------- /extension/logo/JesteerLogo_32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/extension/logo/JesteerLogo_32px.png -------------------------------------------------------------------------------- /extension/logo/JesteerLogo_48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/extension/logo/JesteerLogo_48px.png -------------------------------------------------------------------------------- /extension/logo/JesteerLogo_64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/extension/logo/JesteerLogo_64px.png -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jesteer", 3 | "description": "Imagine a world where tests write themselves.", 4 | "version": "0.0.0.1", 5 | "manifest_version": 3, 6 | "background": { 7 | "service_worker": "background/background.js", 8 | "type": "module" 9 | }, 10 | "permissions": [ 11 | "storage", 12 | "activeTab", 13 | "scripting" 14 | ], 15 | "host_permissions": [ 16 | "" 17 | ], 18 | "action": { 19 | "default_popup": "popup/popup.html", 20 | "default_icon": { 21 | "16": "/logo/JesteerLogo_16px.png", 22 | "32": "/logo/JesteerLogo_32px.png", 23 | "48": "/logo/JesteerLogo_48px.png", 24 | "64": "/logo/JesteerLogo_64px.png", 25 | "128": "/logo/JesteerLogo_128px.png" 26 | } 27 | }, 28 | "icons": { 29 | "16": "/logo/JesteerLogo_16px.png", 30 | "32": "/logo/JesteerLogo_32px.png", 31 | "48": "/logo/JesteerLogo_48px.png", 32 | "64": "/logo/JesteerLogo_64px.png", 33 | "128": "/logo/JesteerLogo_128px.png" 34 | } 35 | } -------------------------------------------------------------------------------- /extension/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 17 | 21 | 22 |

Jesteer

23 | 31 |
32 |
33 | 41 | 49 | 57 |
58 | 59 |
60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /extension/popup/popup.js: -------------------------------------------------------------------------------- 1 | const codegen = document.querySelector('#codegen'); 2 | 3 | document.addEventListener('DOMContentLoaded', async () => { 4 | await chrome.storage.local.get('currentTest', ({ currentTest }) => { 5 | codegen.value = currentTest ?? ''; 6 | }); 7 | }); 8 | 9 | codegen.addEventListener('change', () => { 10 | chrome.storage.local.set({ currentTest: codegen.value }); 11 | }); 12 | 13 | document.querySelector('#btnCopy').addEventListener('click', async () => { 14 | const code = codegen.value; 15 | await navigator.clipboard.writeText(code); 16 | document.querySelector('#btnCopyValue').innerText = 'Copied!'; 17 | }); 18 | -------------------------------------------------------------------------------- /extension/popup/recording.js: -------------------------------------------------------------------------------- 1 | // Handles logic for recording browser actions 2 | import { toggleListeners } from '../content_scripts/toggleListeners.js'; 3 | 4 | // Toggles the text on the Record Button to reflect the given Recording Status rec 5 | const recordButtonUpdate = (rec) => { 6 | document.querySelector('#btnSnapshot').disabled = !rec; 7 | document.querySelector('#btnRecordValue').innerText = rec ? 'Stop Recording' : 'Record'; 8 | }; 9 | 10 | // main function to be executed when record button is pressed 11 | async function execute(tab) { 12 | // Toggle recording status and save in local storage 13 | let { recording } = await chrome.storage.local.get({ recording: false }); 14 | recording = !recording; 15 | await chrome.storage.local.set({ recording }); 16 | // Update the recording button 17 | recordButtonUpdate(recording); 18 | 19 | // click 'Record' 20 | if (recording) { 21 | // send 'initialURL' action to background.js, telling Jesteer to insert 22 | // a page.goto(${ initialURL }) command 23 | await chrome.runtime.sendMessage({ type: 'log', text: `initialURL: ${tab.url}` }); 24 | await chrome.runtime.sendMessage({ type: 'recordAction', action: { type: 'initialURL', url: tab.url } }); 25 | 26 | // if not testing, dismiss the popup 27 | if (navigator.userAgent !== 'PuppeteerAgent') { 28 | window.close(); 29 | } 30 | } 31 | 32 | // click 'Stop recording' 33 | else { 34 | // send stopRecording message to background.js 35 | await chrome.runtime.sendMessage({ type: 'log', text: 'attempt to stop recording from recording.js' }); 36 | 37 | // sending the stopRecording message will trigger a generated test suite as a response 38 | const { output } = await chrome.runtime.sendMessage({ type: 'stopRecording' }); 39 | 40 | // populate codegen box with the generated test suite 41 | document.querySelector('#codegen').value = output; 42 | 43 | // save the generated test suite in Chrome's local memory 44 | await chrome.storage.local.set({ currentTest: output }); 45 | } 46 | 47 | // Insert code for functions shared across popup 48 | chrome.scripting.executeScript({ 49 | target: { tabId: tab.id }, 50 | files: ['content_scripts/common.js'], 51 | }); 52 | 53 | // Tell Chrome to inject event listeners into current page, which will record browser interactions 54 | await chrome.scripting.executeScript({ 55 | target: { tabId: tab.id }, 56 | function: toggleListeners, 57 | args: [recording], 58 | }); 59 | } 60 | 61 | // Populate record button with correct message, depending on recording status 62 | chrome.storage.local.get('recording', ({ recording }) => { 63 | recordButtonUpdate(recording); 64 | }); 65 | 66 | // add event listener for clicking record Button 67 | // acts slightly differently depending on whether or not we are testing 68 | document.querySelector('#btnRecord').addEventListener('click', async () => { 69 | // options that help us decide which tab to act on, depending on whether we're testing or not 70 | const QUERY_TAB_OPTS = { currentWindow: true, active: true }; 71 | const E2E_QUERY_TAB_OPTS = { currentWindow: true, active: false }; 72 | 73 | chrome.tabs.getCurrent(async (tab) => { 74 | const isRunningExtensionOnBrowserTab = !!tab; 75 | // when testing, the extension popup runs in a separate browser tab 76 | const opts = isRunningExtensionOnBrowserTab ? E2E_QUERY_TAB_OPTS : QUERY_TAB_OPTS; 77 | const tabIndex = isRunningExtensionOnBrowserTab ? 1 : 0; 78 | 79 | chrome.tabs.query(opts, (tabs) => execute(tabs[tabIndex])); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /extension/popup/snapshot.js: -------------------------------------------------------------------------------- 1 | // Javascript Components for the popup page when the extension icon is clicked 2 | 3 | // Injects the event listeners that select/deselect DOM elements 4 | function prepareSnapshot() { 5 | // Helper functions for selecting and deselecting elements 6 | const select = (e) => e.target.setAttribute('___jesteer___highlight', ''); 7 | const deselect = (e) => e.target.removeAttribute('___jesteer___highlight'); 8 | 9 | // Generate the snapshot 10 | const snap = (e) => { 11 | e.preventDefault(); 12 | deselect(e); 13 | 14 | const selectorPath = getSelectorPath(e.target); 15 | 16 | const action = { type: 'snapshot', element: selectorPath }; 17 | chrome.runtime.sendMessage({ type: 'recordAction', action }); 18 | 19 | // Stop the event listeners after the snapshot is generated 20 | document.removeEventListener('mouseover', select); 21 | document.removeEventListener('mouseout', deselect); 22 | document.removeEventListener('click', snap); 23 | }; 24 | 25 | // Add Event Listeners for Highlighting and Logging-on-Click 26 | document.addEventListener('mouseover', select); 27 | document.addEventListener('mouseout', deselect); 28 | document.addEventListener('click', snap); 29 | } 30 | 31 | const btnSnapshot = document.querySelector('#btnSnapshot'); 32 | 33 | btnSnapshot.addEventListener('click', async () => { 34 | const testing = (navigator.userAgent === 'PuppeteerAgent'); 35 | 36 | let tab; 37 | if (testing) { 38 | const tabs = (await chrome.tabs.query({ currentWindow: true, active: false })); 39 | const index = 1; // when testing, popup.html is at tab index 1 40 | tab = tabs[index]; 41 | } 42 | else { 43 | const tabs = (await chrome.tabs.query({ currentWindow: true, active: true })); 44 | const index = 0; // when not testing, popup.html is at tab index 0 45 | tab = tabs[index]; 46 | } 47 | 48 | // Execute the 'snapshot' function in the context of the current webpage 49 | chrome.scripting.executeScript({ 50 | target: { tabId: tab.id }, 51 | function: prepareSnapshot, 52 | }); 53 | 54 | // Add styling to the attribute given to elements we're hovering over 55 | chrome.scripting.insertCSS({ 56 | target: { tabId: tab.id }, 57 | css: '*[___jesteer___highlight] { background-color: yellow !important; }', 58 | }); 59 | 60 | // Dismiss the popup if not testing 61 | if (navigator.userAgent !== 'PuppeteerAgent') { 62 | window.close(); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /extension/popup/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); 2 | 3 | * { 4 | padding: 0; 5 | margin: 0; 6 | box-sizing: border-box; 7 | font-family: 'Inter'; 8 | } 9 | 10 | html, 11 | body { 12 | position: relative; 13 | } 14 | 15 | body { 16 | border: 1px solid black; 17 | background-color: #F8F8F8; 18 | display: inline-block; 19 | } 20 | 21 | a { 22 | cursor: pointer; 23 | } 24 | 25 | a:hover { 26 | opacity: 75%; 27 | } 28 | 29 | .title { 30 | width: 100%; 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between; 34 | border-bottom: 1px solid black; 35 | background-color: white; 36 | padding: 4px; 37 | } 38 | 39 | .title h1 { 40 | font-size: 16px; 41 | user-select: none; 42 | } 43 | 44 | .title svg { 45 | padding-left: 0.25em; 46 | padding-right: 0.25em; 47 | } 48 | 49 | .title a { 50 | display: flex; 51 | align-items: center; 52 | } 53 | 54 | .action-button { 55 | display: flex; 56 | align-items: center; 57 | justify-content: space-between; 58 | border-radius: 5px; 59 | box-shadow: 0px 4px 4px 0px #00000040; 60 | border: 1px solid black; 61 | font-size: 12px; 62 | background-color: white; 63 | width: 350px; 64 | margin: 1em auto; 65 | } 66 | 67 | .action-button:not([disabled]):hover { 68 | opacity: 80%; 69 | background: rgba(0, 174, 142, 0.4); 70 | font-weight: 600; 71 | } 72 | 73 | .action-button:not([disabled]):active { 74 | background: rgba(204, 0, 0, 0.4); 75 | font-weight: 600; 76 | box-shadow: 0px 0px 0px 0px; 77 | } 78 | 79 | .action-button:not([disabled]) { 80 | cursor: pointer; 81 | } 82 | 83 | .button-icon { 84 | width: 30px; 85 | height: 28px; 86 | border-right: 1px solid black; 87 | display: flex; 88 | align-items: center; 89 | justify-content: center; 90 | } 91 | 92 | .button-name { 93 | margin: 0 auto; 94 | } 95 | 96 | #codegen-container { 97 | display: flex; 98 | justify-content: center; 99 | } 100 | 101 | #codegen { 102 | height: 400px; 103 | width: 500px; 104 | resize: none; 105 | margin: 0 20px 20px; 106 | font-family: monospace; 107 | border: 1px solid black; 108 | border-radius: 3px; 109 | white-space: nowrap; 110 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // Sync object 2 | /** @type {import('@jest/types').Config.InitialOptions} */ 3 | const config = { 4 | verbose: true, 5 | testTimeout: 15000, 6 | rootDir: 'e2e', 7 | }; 8 | 9 | module.exports = config; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "jest": "^28.0.3", 4 | "puppeteer": "^13.7.0", 5 | "sass": "^1.51.0" 6 | }, 7 | "scripts": { 8 | "test": "jest extension.spec.js " 9 | }, 10 | "devDependencies": { 11 | "eslint": "^8.15.0", 12 | "eslint-config-airbnb-base": "15.0.0", 13 | "eslint-plugin-import": "^2.26.0", 14 | "puppeteer-to-istanbul": "^1.4.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /splash/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /splash/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "splash", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "rollup -c -w", 8 | "start": "sirv public", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "svelte": "^3.47.0" 16 | }, 17 | "devDependencies": { 18 | "@rollup/plugin-commonjs": "^17.0.0", 19 | "@rollup/plugin-node-resolve": "^11.0.0", 20 | "rollup": "^2.3.4", 21 | "rollup-plugin-css-only": "^3.1.0", 22 | "rollup-plugin-livereload": "^2.0.0", 23 | "rollup-plugin-svelte": "^7.0.0", 24 | "rollup-plugin-terser": "^7.0.0", 25 | "sirv-cli": "^2.0.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /splash/public/build/bundle.css: -------------------------------------------------------------------------------- 1 | h2.svelte-1ncdnxm{text-align:center}.howTo-container.svelte-1ncdnxm{display:flex;flex-direction:column;margin-top:3rem;gap:2rem}.howTo.svelte-1ncdnxm{display:flex;justify-content:space-around;align-items:flex-start}.howTo-text.svelte-1ncdnxm{width:40%;border-right:1px solid black;padding-right:1.5rem}hr.svelte-1ncdnxm{width:80%;margin-left:10%;margin-right:10%;margin-bottom:1.5rem;height:1px;border-width:0;color:black;background-color:black}h2.svelte-gjdiri{text-align:center;margin-bottom:1.5rem}.team-container.svelte-gjdiri{margin-top:8rem}img.svelte-gjdiri{clip-path:circle(50px at center)}.team.svelte-gjdiri{display:flex;justify-content:center;gap:10px;padding:4rem;text-align:center}.member.svelte-gjdiri{flex:1}hr.svelte-gjdiri{width:80%;margin-left:10%;margin-right:10%;margin-bottom:1.5rem;height:1px;border-width:0;color:black;background-color:black}.nav-container.svelte-uhhdyg.svelte-uhhdyg{margin:2.2rem auto;width:70%}ul.svelte-uhhdyg.svelte-uhhdyg{list-style:none;display:flex;justify-content:space-between;align-items:center}li.svelte-uhhdyg a.svelte-uhhdyg{position:relative;font-weight:1.6rem}li.svelte-uhhdyg a.svelte-uhhdyg:after{position:absolute;content:"";left:0;bottom:-1rem;width:50%;background:#333;transition:height 0.4s}a.svelte-uhhdyg.svelte-uhhdyg:hover:after{height:3px;transition:height 0.4s}.nav-brand.svelte-uhhdyg.svelte-uhhdyg{font-weight:700;font-size:3rem}hr.svelte-uhhdyg.svelte-uhhdyg{margin-top:1.6rem;width:100%;height:2px;border-width:0;color:black;background-color:black}footer.svelte-m7e46{display:flex;justify-content:center;text-align:center;background-color:black;color:white;padding:2rem;font-size:small}a.svelte-m7e46{color:white;text-decoration:none}.header.svelte-190c85l.svelte-190c85l{padding-top:7rem;text-align:center}.header.svelte-190c85l h1.svelte-190c85l{font-size:6rem;font-weight:800}.header.svelte-190c85l p.svelte-190c85l{padding-top:1.5rem;display:inline-block;width:35%;font-size:3rem;color:#333}.info.svelte-190c85l.svelte-190c85l{padding-top:4rem;display:flex;justify-content:center}.info-media.svelte-190c85l.svelte-190c85l{width:50%;position:absolute}.info-text.svelte-190c85l.svelte-190c85l{width:40%;padding-left:5rem;line-height:3rem}.info-text.svelte-190c85l a.svelte-190c85l{line-height:7rem;border-bottom:2px solid #000}.info-text.svelte-190c85l a.svelte-190c85l:hover{color:rgb(6, 5, 5);font-weight:700} -------------------------------------------------------------------------------- /splash/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/favicon.png -------------------------------------------------------------------------------- /splash/public/global.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800;900&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Inter', sans-serif; 7 | } 8 | 9 | html, body { 10 | scroll-behavior: smooth; 11 | box-sizing: border-box; 12 | font-size: 62.5%; 13 | } 14 | 15 | body { 16 | color: #000; 17 | font-size: 2rem; 18 | background-image: linear-gradient(rgba(204, 0, 0, 0.4), rgba(0, 174, 142, 0.4), rgba(0, 146, 58, 0.4)), url("./img/noise-lines.png"); 19 | } 20 | 21 | .container { 22 | width: 80%; 23 | height: 100vh; 24 | margin: auto; 25 | } 26 | 27 | a { 28 | color: #000; 29 | text-decoration: none; 30 | } 31 | -------------------------------------------------------------------------------- /splash/public/img/640px.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/640px.gif -------------------------------------------------------------------------------- /splash/public/img/GitHubLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/GitHubLogo.png -------------------------------------------------------------------------------- /splash/public/img/JesteerLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /splash/public/img/JesteerLogo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/JesteerLogo2.png -------------------------------------------------------------------------------- /splash/public/img/LinkedInLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/LinkedInLogo.png -------------------------------------------------------------------------------- /splash/public/img/cha.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/cha.jpeg -------------------------------------------------------------------------------- /splash/public/img/clare.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/clare.jpeg -------------------------------------------------------------------------------- /splash/public/img/dark-denim-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/dark-denim-3.png -------------------------------------------------------------------------------- /splash/public/img/katie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/katie.png -------------------------------------------------------------------------------- /splash/public/img/noise-lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/noise-lines.png -------------------------------------------------------------------------------- /splash/public/img/noise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/noise.png -------------------------------------------------------------------------------- /splash/public/img/tim.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Jesteer/012a018ed42465001b1e2b34dff3f3c40e93db0e/splash/public/img/tim.jpeg -------------------------------------------------------------------------------- /splash/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jesteer 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /splash/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import css from 'rollup-plugin-css-only'; 7 | 8 | const production = !process.env.ROLLUP_WATCH; 9 | 10 | function serve() { 11 | let server; 12 | 13 | function toExit() { 14 | if (server) server.kill(0); 15 | } 16 | 17 | return { 18 | writeBundle() { 19 | if (server) return; 20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 21 | stdio: ['ignore', 'inherit', 'inherit'], 22 | shell: true 23 | }); 24 | 25 | process.on('SIGTERM', toExit); 26 | process.on('exit', toExit); 27 | } 28 | }; 29 | } 30 | 31 | export default { 32 | input: 'src/main.js', 33 | output: { 34 | sourcemap: true, 35 | format: 'iife', 36 | name: 'app', 37 | file: 'public/build/bundle.js' 38 | }, 39 | plugins: [ 40 | svelte({ 41 | compilerOptions: { 42 | // enable run-time checks when not in production 43 | dev: !production 44 | } 45 | }), 46 | // we'll extract any component CSS out into 47 | // a separate file - better for performance 48 | css({ output: 'bundle.css' }), 49 | 50 | // If you have external dependencies installed from 51 | // npm, you'll most likely need these plugins. In 52 | // some cases you'll need additional configuration - 53 | // consult the documentation for details: 54 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 55 | resolve({ 56 | browser: true, 57 | dedupe: ['svelte'] 58 | }), 59 | commonjs(), 60 | 61 | // In dev mode, call `npm run start` once 62 | // the bundle has been generated 63 | !production && serve(), 64 | 65 | // Watch the `public` directory and refresh the 66 | // browser on changes when not in production 67 | !production && livereload('public'), 68 | 69 | // If we're building for production (npm run build 70 | // instead of npm run dev), minify 71 | production && terser() 72 | ], 73 | watch: { 74 | clearScreen: false 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /splash/src/App.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |