├── .gitignore ├── README.md ├── e2e ├── helpers.js ├── jestConfig.json ├── pageObjects │ ├── app.js │ └── index.js ├── setupTests.js └── specs │ ├── app.js │ └── index.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | e2e/selenium 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project demonstrates one way to add Selenium e2e tests to a `create-react-app` generated app. It uses Jest as the test runner, embraces modern JavaScript features (including async/await, modules, fat-arrow functions, templated strings, etc.) and attempts to ensure that modern editors are able to provide useful auto-complete suggestions when authoring tests. 2 | 3 | # Contents 4 | 5 | * [Running Specs](#running-specs) 6 | * [Writing Specs](#writing-specs) 7 | * [Writing Page Objects](#writing-page-objects) 8 | * [Adding to an Existing Project](#adding-to-an-existing-project) 9 | 10 | # Disclaimer 11 | 12 | This project isn't for everyone. In fact, if you're trying to add your first tests to a `create-react-app`, I'd go as far as to say it's almost definitely not for you. Instead I'd recommend reading over the [User Guide](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md) first for a testing approach that's easier to work with and much faster to run! 13 | 14 | However, sometimes I've found Jest's jsdom testing isn't enough and I've needed to run tests in real browsers e.g. when taking performance measurements, testing a full stack, smoke testing the live system, testing functionality in a specific browser, etc.. For those cases, this project presents one possible solution that does not require you to `eject` yet still embraces modern JavaScript features and tooling. Specifically it allows you to share a consistent code style across your `e2e` code and your `src` code by running the same babel transforms and using the same test runner. 15 | 16 | # Running Specs 17 | 18 | Let's jump straight in to running the example specs, open a new terminal and run the following (you can substitute `npm` for `yarn` if you prefer) - 19 | 20 | ```bash 21 | $ yarn install 22 | $ yarn run e2e-update 23 | $ yarn run e2e-start 24 | ``` 25 | 26 | Under the covers, the project uses `webdriver-manager` to manage the Selenium server. The first script uses it to download the latest Selenium package and drivers for your browsers. The second starts up a new local Selenium server which you'll need to leave running otherwise the tests won't have a Selenium server to connect to. 27 | 28 | We need something to test, so let's open a new terminal and serve up the app - 29 | 30 | ```bash 31 | $ yarn start 32 | ``` 33 | 34 | This will build the app and open it in a browser as normal. Remember not to close the terminal or you'll see errors about not being able to load page. 35 | 36 | Finally let's fire up another new terminal and kick off Jest to run the tests for us - 37 | 38 | ```bash 39 | $ yarn run e2e 40 | ``` 41 | 42 | You should hopefully see the in progress state of the tests in the terminal and Chrome should open and close a few times as the tests run. 43 | 44 | ## Automatically running the tests 45 | 46 | As well as running the tests once, you can also start Jest in watch mode to automatically run your tests whenever they change by running - 47 | 48 | ```bash 49 | $ yarn run e2e -- --watch 50 | ``` 51 | 52 | ## Running the tests sequentially 53 | 54 | You can also pass any other arguments through to Jest in this fashion. For example, by default Jest will attempt to run many test files in parallel to speed things up but if you didn't want that to happen you can force them to run sequentially with - 55 | 56 | ```bash 57 | $ yarn run e2e -- --runInBand 58 | ``` 59 | 60 | ## Using environment specific values 61 | 62 | Values can be injected in to the tests by specifying them as globals when running Jest - 63 | 64 | ```bash 65 | $ yarn run e2e -- --globals "{\"baseUrl\": \"https://example.com\"} 66 | ``` 67 | 68 | You can configure the default values for these globals and other Jest settings in `e2e/jestConfig.json`. 69 | 70 | # Writing Specs 71 | 72 | The spec files can be found in `e2e/specs`, let's look at `index.js` as an example of how to write a simple spec - 73 | 74 | ```js 75 | import { driver } from '../helpers'; 76 | import { load } from '../pageObjects/index'; 77 | 78 | describe('index', () => { 79 | it('should show the right title', async () => { 80 | await load(); 81 | expect(await driver.getTitle()).toBe('React App'); 82 | }); 83 | }); 84 | ``` 85 | 86 | Let's run through what's going on line by line - 87 | 88 | ```js 89 | import { driver } from '../helpers'; 90 | ``` 91 | 92 | Here we're using the new ES2016 module syntax to import a function from the `helpers.js` file. The old syntax for this would have looked something like `var driver = require('../helpers').driver`. Not only is the new syntax easier on the fingers and a bit nicer to look at, it also allows editors to be more confident about what you're importing and offer features like smarter auto-complete. 93 | 94 | In this project the `helpers.js` file exposes a few basic utilities. The most important is `driver` (an instance of `WebDriver` from the `selenium-webdriver` package) which has all the Selenium methods you'd expect (`findElement`, `get`, etc.). N.B. you don't need to explicitly call `driver.quit()`, this is automatically invoked from an `afterAll` handler in `helpers.js`. 95 | 96 | ```js 97 | import { load } from '../pageObjects/index'; 98 | ``` 99 | 100 | Again here we're using the new module syntax to import the `load` method from `index.js` in the `pageObject` folder. 101 | 102 | ```js 103 | describe('index', () => { 104 | ``` 105 | 106 | Jest uses a customised implementation of Jasmine. If you've not used Jasmine before it's enough to know that the global `describe` method is used to logically group tests together under a common label (in this case `'index'`). Within each group, the `afterAll`/`beforeAll`/`afterEach`/`beforeEach` global methods can be used to define functions which only apply to the group's tests. 107 | 108 | The symbol heavy part of the line `() => {` and the matching `}` a few lines later is the new ES2015 syntax for declaring functions. The old syntax for this would have looked something like `function () {` and `}`. 109 | 110 | In this particular case there are no arguments and no return value, so the syntax isn't that much more succinct. However, throw in an argument and the new syntax `a => { }` starts looking a little bit nicer than the old one `function (a) { }`. Additionally throw in a return value `a => { return a; }` and we can eliminate all of the brackets: `a => a`. 111 | 112 | Be aware though, that the parentheses are back when you have multiple arguments e.g. `(a, b) => { }`. 113 | 114 | ```js 115 | it('should show the right title', async () => { 116 | ``` 117 | 118 | Again if you're familiar with Jasmine you'll probably recognise the `it` method. If not, it's job is to associate a label with the specification of the behaviour you're intending to test. 119 | 120 | The new ES2017 feature in this line is `async`. Used before a function, it marks the function as running asynchronously. This is useful when a function needs to wait for some external input e.g. from the file system, network or in this case from the browser. 121 | 122 | When calling an `async` function, instead of receiving the return value directly, you receive a promise which will `resolve` to the return value. That is just a wordy way of saying that, using the syntax we just learnt, `async a => a` is equivalent to `a => Promise.resolve(a)`. Whilst it's certainly less typing, to see the true value of `async` functions, you'll need to keep reading. 123 | 124 | ```js 125 | await load(); 126 | ``` 127 | 128 | Here we're calling the `load` page object method we previously imported. However, we're making use of `await` to make the function wait until the promise returned by `load` is resolved before continuing. Before we dig into the details let's look at the more complex example on the next line. 129 | 130 | ```js 131 | expect(await driver.getTitle()).toBe('React App'); 132 | ``` 133 | 134 | The `expect` and `toBe` methods are provided by `jasmine` and assert the equality of their respective arguments. The string `'React App'` is *to be expected* (pun intended) but the use of `await driver.getTitle()` might be more unexpected. 135 | 136 | Previously, this would have required a far more complex implementation `driver.getTitle().then(title => expect(title).toBe('React App'))`. Even using the new fat arrow syntax it's a lot less readable and that's before any chaining of promises! 137 | 138 | ```js 139 | }); 140 | }); 141 | ``` 142 | 143 | If you've ever been confused in the past about when to use `then`, when you can let `selenium-webdriver`'s promise manager do it's magic or why your `console.log` calls are happening at weird times, you'll be glad to know the behaviour is now much simpler. Any time you invoke an asynchronous method (most calls to the `selenium-webdriver` API and any page object methods which use them), first check to see that the function is marked `async` and then prefix the call with `await`. No more magic. 144 | 145 | # Writing Page Objects 146 | 147 | The page object files can be found in `e2e/pageObjects`, let's look at `index.js` as an example of how to write a simple spec - 148 | 149 | ```js 150 | import { until } from 'selenium-webdriver'; 151 | import { driver, defaultTimeout } from '../helpers'; 152 | 153 | const rootSelector = { css: '#root' }; 154 | 155 | export const root = () => driver.findElement(rootSelector); 156 | 157 | export const load = async () => { 158 | await driver.get(`${__baseUrl__}/`); 159 | await driver.wait(until.elementLocated(root), defaultTimeout); 160 | }; 161 | ``` 162 | 163 | Again let's take things line by line however, as we introduced imports in the last section, let's use the imports here as an excuse to introduce some useful variations on the import syntax - 164 | 165 | ```js 166 | import { until } from 'selenium-webdriver'; 167 | ``` 168 | 169 | Sometimes you'll need to import two different things from two different modules that share the same name. In such a situation you have two options: alias the import or import the module's exports as an object. 170 | 171 | If in the line above we wanted to import until with a custom name e.g. `untilSW`, we might previously have used something like `var untilSW = require('selenium-webdriver').until` With the new syntax we can alias the import using `import { until: untilSW } from 'selenium-webdriver'`. 172 | 173 | If it looks a little odd, it's actually borrowed from another feature called object destructuring. A fancy way of describing that if we have an variable `a` with the value `{ b: 1, c: 2 }`, the following are equivalent `const B = a.b, C = a.c` and `const { b: B, c: C } = a`. It can take a bit of getting used to, it looked backwards to me when I first saw it, but once you're used to it it be used to produce more expressive code. 174 | 175 | N.B. Whilst this particular feature is borrowed from object destructuring, it is not *using* object destructuring i.e. not every feature from one will work with the other. 176 | 177 | ```js 178 | import { driver, defaultTimeout } from '../helpers'; 179 | ``` 180 | 181 | Let's use this line to demonstrate importing a module's exports as an object: `import * as helpers from '../helpers'`. This is very similar, although as I'll explain subtly different, to the old syntax of `var helpers = require('../helpers')`. It allows us to avoid potential naming conflicts by accessing the exports at `helpers.driver` and `helpers.defaultTimeout` respectively. 182 | 183 | The subtle difference that we skipped over relates to the concept of `default` exports. A module can choose to export one thing as its `default` as well as any number of named exports which is the syntax we've introduced so far. 184 | 185 | Exporting defaults is easy. As an example, let's convert `export const foo = 'bar'` to be a default export instead: `export default 'bar'`. It's equally easy to import the default `import foo from 'module'` and you can also mix default and named exports using `import someDefaultExport, { someNamedExport } from 'module'`. 186 | 187 | That's enough module syntax, on to the real code! 188 | 189 | ```js 190 | const rootSelector = { css: '#root' }; 191 | ``` 192 | 193 | First up, we declare a css-based element selector using the `selenium-webdriver` object shorthand. You may be more familiar with declaring variables using `var` rather than `let` or `const` which were introduced in ES2015. Whilst similar to `var` they are subtly different, they both prevent unexpected errors by restricting their definition to the enclosing block (`{}`) e.g. `if (true) { let b = 0; } b = 1 /* throws an error */`. 194 | 195 | The difference between `let` and `const` is more stark. A variable declared using `let` can be assigned to many times e.g. `let a = 1; a = 2;`. Whereas a variable declared using `const` can only be assigned to when declaring it e.g. `const a = 1; a = 2 /* throws an error */`. I would personally always recommend using `const` unless you explicitly need to reassign the variable. This makes it easier for the next person (probably yourself!) to read and reason about the functionality of your code. 196 | 197 | ```js 198 | export const root = () => driver.findElement(rootSelector); 199 | ``` 200 | 201 | This line exports a `root` function which makes use of the `driver.findElement` asynchronous function together with the css-based element selector declared above to return a promise that resolves to an element reference. Previously exports in Node.js used either `exports.root = ` or `module.exports.root = ` but again the new syntax is clearer and makes it easier for tools to know what your intention is. 202 | 203 | You might be wondering why when I introduced `async`/`await` in the last section and I suggested you should use it for all asynchronous function calls but I haven't here. The reason is that I could use it here but it would produce the same result i.e. `async () => await driver.findElement(rootSelector)` is equivalent to `() => driver.findElement(rootSelector)`. The verbose version creates a promise and waits for the `driver.findElement` promise to resolve, it then uses the resolved value to resolve the promise it created. All in all, more typing and more work for the JavaScript engine for no real benefit. 204 | 205 | ```js 206 | export const load = async () => { 207 | ``` 208 | 209 | Here we define another exported function `load` but this time we're back to using await as we need to wait for one asynchronous function to complete and then wait for another to complete. 210 | 211 | ```js 212 | await driver.get(`${__baseUrl__}/`); 213 | ``` 214 | 215 | The first asynchronous function `driver.get` tells Selenium to loads a URL in the browser. The URL includes a global defined by Jest, see [using environment specific values](#using-environment-specific-values) for where this value comes from. 216 | 217 | The specification of the URL ``${__baseUrl__}/`` is using an ES2015 feature called template strings. Previously you might have written this as a concatenation of two strings `__baseUrl + '/'`. In this case there's very little difference in key strokes or clarity but with strings featuring more variables the new syntax is much nicer e.g. ``Date: ${a}-${b}-${c}``. 218 | 219 | ```js 220 | await driver.wait(until.elementLocated(root), defaultTimeout); 221 | ``` 222 | 223 | The second asynchronous function `driver.wait` tells Selenium to wait until an element is found in the browser's DOM. It makes use of a utility `until` from `selenium-webdriver` which checks returns a boolean indicating the presence of an element and `defaultTimeout` which is provider by `helpers` to allow timeouts to be scaled for debugging. 224 | 225 | ```js 226 | }; 227 | ``` 228 | 229 | Embracing both `async`/`await` and modules really helps simplify page objects and improve their legibility. It is a much more productive and far less frustrating experience when having written tests or any code, the next person who comes along with no knowledge of the project, can easily understand and work with the code without assistance. 230 | 231 | And as we all know, that next person is probably you in a months time! 232 | 233 | # Adding to an Existing Project 234 | 235 | To convert an existing `create-react-app` generated project to use these features - 236 | 237 | * Copy the `e2e` folder into the root of the project 238 | ```bash 239 | $ cp -r react-app-webdriver/e2e 240 | ``` 241 | * Install the additional dev dependencies - 242 | ```bash 243 | $ yarn add --dev @types/jest jest selenium-webdriver webdriver-manager 244 | ``` 245 | * Add the following to the `scripts` section in the project's `package.json` - 246 | ```js 247 | { 248 | // ... 249 | "scripts: { 250 | // ... 251 | "e2e": "jest -c e2e/jestConfig.json", 252 | "e2e-update": "webdriver-manager update --out_dir ../../e2e/selenium", 253 | "e2e-start": "webdriver-manager start --out_dir ../../e2e/selenium" 254 | } 255 | } 256 | ``` 257 | -------------------------------------------------------------------------------- /e2e/helpers.js: -------------------------------------------------------------------------------- 1 | import { Builder } from 'selenium-webdriver'; 2 | 3 | export const driver = new Builder() 4 | .forBrowser('chrome') 5 | .usingServer('http://localhost:4444/wd/hub') 6 | .build(); 7 | 8 | afterAll(async () => { 9 | // Cleanup `process.on('exit')` event handlers to prevent a memory leak caused by the combination of `jest` & `tmp`. 10 | for (const listener of process.listeners('exit')) { 11 | listener(); 12 | process.removeListener('exit', listener); 13 | } 14 | await driver.quit(); 15 | }); 16 | 17 | export const defaultTimeout = 10e3; 18 | -------------------------------------------------------------------------------- /e2e/jestConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "setupTestFrameworkScriptFile": "./setupTests.js", 3 | "testMatch": [ 4 | "**/specs/**/*.js" 5 | ], 6 | "transform": { 7 | "\\.js$": "react-scripts/config/jest/babelTransform" 8 | }, 9 | "globals": { 10 | "__baseUrl__": "http://localhost:3000" 11 | } 12 | } -------------------------------------------------------------------------------- /e2e/pageObjects/app.js: -------------------------------------------------------------------------------- 1 | import { root } from './index'; 2 | 3 | const introSelector = { css: '.App-intro' }; 4 | const headerSelector = { css: '.App-header > h2' }; 5 | 6 | export const intro = () => root().findElement(introSelector); 7 | 8 | export const header = () => root().findElement(headerSelector); 9 | -------------------------------------------------------------------------------- /e2e/pageObjects/index.js: -------------------------------------------------------------------------------- 1 | import { until } from 'selenium-webdriver'; 2 | import { driver, defaultTimeout } from '../helpers'; 3 | 4 | const rootSelector = { css: '#root' }; 5 | 6 | export const root = () => driver.findElement(rootSelector); 7 | 8 | export const load = async () => { 9 | await driver.get(`${__baseUrl__}/`); 10 | await driver.wait(until.elementLocated(root), defaultTimeout); 11 | }; 12 | -------------------------------------------------------------------------------- /e2e/setupTests.js: -------------------------------------------------------------------------------- 1 | process.env.USE_PROMISE_MANAGER = false; 2 | 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60e3; 4 | -------------------------------------------------------------------------------- /e2e/specs/app.js: -------------------------------------------------------------------------------- 1 | import { intro, header } from '../pageObjects/app'; 2 | import { load } from '../pageObjects/index'; 3 | 4 | describe('app', () => { 5 | beforeEach(async () => { 6 | await load(); 7 | }); 8 | 9 | it('should show the right intro', async () => { 10 | expect(await intro().getText()).toBe( 11 | 'To get started, edit src/App.js and save to reload.' 12 | ); 13 | }); 14 | 15 | it('should show the right header', async () => { 16 | expect(await header().getText()).toBe('Welcome to React'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /e2e/specs/index.js: -------------------------------------------------------------------------------- 1 | import { driver } from '../helpers'; 2 | import { load } from '../pageObjects/index'; 3 | 4 | describe('index', () => { 5 | it('should show the right title', async () => { 6 | await load(); 7 | expect(await driver.getTitle()).toBe('React App'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app-webdriver", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^15.6.1", 7 | "react-dom": "^15.6.1" 8 | }, 9 | "devDependencies": { 10 | "@types/jest": "^19.2.3", 11 | "jest": "^20.0.4", 12 | "react-scripts": "1.0.7", 13 | "selenium-webdriver": "^3.4.0", 14 | "webdriver-manager": "^12.0.6" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject", 21 | "e2e": "jest -c e2e/jestConfig.json", 22 | "e2e-update": "webdriver-manager update --out_dir ../../e2e/selenium", 23 | "e2e-start": "webdriver-manager start --out_dir ../../e2e/selenium" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisprice/react-app-webdriver/2c3ffb1c44c0acbed8e9187edbf2a324c58452e8/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 | logo 11 |

Welcome to React

12 |
13 |

14 | To get started, edit src/App.js and save to reload. 15 |

16 |
17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | import './index.css'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | --------------------------------------------------------------------------------