Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more Web Platform Tests #120

Merged
merged 24 commits into from
Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ab80b18
Add rest of Web Platform Tests
koddsson Feb 22, 2021
88f51a6
Expose `assert.deepEqual` to Web Platform Tests
koddsson Feb 22, 2021
0560186
`__Secure` prefixed cookies are secure
koddsson Feb 22, 2021
0dd2309
Set `sameSite` to `lax` when item is secure
koddsson Feb 22, 2021
409bc0b
Set path to `/` when name is prefixed with __Host
koddsson Feb 22, 2021
85279a3
Default `changed` and `deleted` to empty arrays
koddsson Feb 22, 2021
50c9152
Make sure to fail when name is empty and value includes `=`
koddsson Feb 22, 2021
1e83ead
Expires can be a date
koddsson Feb 22, 2021
c9159d1
Remove unneeded serviceworker test setup file
koddsson Feb 23, 2021
6db6f33
Move karma single run option into config file
koddsson Feb 23, 2021
9bcd85f
Setup service worker registration for tests
koddsson Feb 23, 2021
cb19e07
CookieMatchType is unused
koddsson Feb 25, 2021
f1f988b
Fix service worker tests
koddsson Mar 1, 2021
b5eb4c4
Implement CookieStoreManager for service workers
koddsson Mar 1, 2021
9554b77
Make sure all the servive worker tests run
koddsson Mar 1, 2021
f8bcf28
Don't throw errors in `getAll`
koddsson Mar 1, 2021
6c2fc44
Clean up ordered cookies after test run
koddsson Mar 1, 2021
a09a8cb
Allow passing of empty object to `getAll`
koddsson Mar 1, 2021
83b036f
Add ability to skip tests that we can't pass right now
koddsson Mar 1, 2021
d259a0f
Don't expose CookieStore on the global object
koddsson Dec 6, 2021
654eeb1
Update README
koddsson Dec 6, 2021
b7328a7
Fix tests
koddsson Dec 6, 2021
7e02439
Update Web Platform Test
koddsson Dec 6, 2021
52bd50b
Set return type of get Symbol method
koddsson Dec 6, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Build Status](https://travis-ci.org/mkay581/cookie-store.svg?branch=master)](https://travis-ci.org/mkay581/cookie-store)
[![npm version](https://badge.fury.io/js/cookie-store.svg)](https://www.npmjs.com/package/cookie-store)

A polyfill to allow use of the [Cookie Store API](https://wicg.github.io/cookie-store/) in modern browsers that don't support it natively, including IE11. Also compatible with TypeScript.
A ponyfill to allow use of the [Cookie Store API](https://wicg.github.io/cookie-store/) in modern browsers that don't support it natively, including IE11. Also compatible with TypeScript.

## Installation

Expand All @@ -15,7 +15,7 @@ npm install cookie-store

```js
// import polyfill and declare types
import 'cookie-store';
import {cookieStore} from 'cookie-store';

// set a cookie
await cookieStore.set('forgive', 'me');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"scripts": {
"pretest": "npm run build",
"test": "npm run test:ts && eslint 'src/**/*' && npm run prettier",
"test:ts": "karma start test/karma.conf.cjs --single-run",
"test:ts": "karma start test/karma.conf.cjs",
"prettier": "prettier --check 'src/**/*'",
"preversion": "npm test",
"banner": "banner-cli dist/index.js",
Expand Down
137 changes: 115 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ function tryDecode(
}
}

type CookieMatchType = 'equals';

interface Cookie {
domain?: string;
expires?: number;
Expand All @@ -34,7 +32,6 @@ interface CookieStoreDeleteOptions {
interface CookieStoreGetOptions {
name?: string;
url?: string;
matchType?: CookieMatchType;
}

interface ParseOptions {
Expand All @@ -52,7 +49,7 @@ interface CookieListItem {
value?: string;
domain: string | null;
path?: string;
expires: number | null;
expires: Date | number | null;
secure?: boolean;
sameSite?: CookieSameSite;
}
Expand Down Expand Up @@ -120,15 +117,15 @@ class CookieChangeEvent extends Event {
eventInitDict: CookieChangeEventInit = { changed: [], deleted: [] }
) {
super(type, eventInitDict);
this.changed = eventInitDict.changed;
this.deleted = eventInitDict.deleted;
this.changed = eventInitDict.changed || [];
this.deleted = eventInitDict.deleted || [];
}
}

class CookieStore extends EventTarget {
onchange?: (event: CookieChangeEvent) => void;

get [Symbol.toStringTag]() {
get [Symbol.toStringTag](): 'CookieStore' {
return 'CookieStore';
}

Expand Down Expand Up @@ -177,9 +174,15 @@ class CookieStore extends EventTarget {
if (item.domain && item.domain !== window.location.hostname) {
throw new TypeError('Cookie domain must domain-match current host');
}
if (item.name === '' && item.value && item.value.includes('=')) {

if (item.name?.startsWith('__Host') && item.domain) {
throw new TypeError(
'Cookie domain must not be specified for host cookies'
);
}
if (item.name?.startsWith('__Host') && item.path != '/') {
throw new TypeError(
"Cookie value cannot contain '=' if the name is empty"
'Cookie path must not be specified for host cookies'
);
}

Expand All @@ -191,22 +194,35 @@ class CookieStore extends EventTarget {
}
}

if (item.name === '' && item.value && item.value.includes('=')) {
throw new TypeError(
"Cookie value cannot contain '=' if the name is empty"
);
}

if (item.name && item.name.startsWith('__Host')) {
item.secure = true;
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let cookieString = `${item.name}=${encodeURIComponent(item.value!)}`;

if (item.domain) {
cookieString += '; Domain=' + item.domain;
}

if (item.path && item.path !== '/') {
if (item.path) {
cookieString += '; Path=' + item.path;
}

if (typeof item.expires === 'number') {
cookieString += '; Expires=' + new Date(item.expires).toUTCString();
} else if (item.expires instanceof Date) {
cookieString += '; Expires=' + item.expires.toUTCString();
}

if (item.secure) {
if ((item.name && item.name.startsWith('__Secure')) || item.secure) {
item.sameSite = CookieSameSite.lax;
cookieString += '; Secure';
}

Expand Down Expand Up @@ -245,14 +261,9 @@ class CookieStore extends EventTarget {
init?: CookieStoreGetOptions['name'] | CookieStoreGetOptions
): Promise<Cookie[]> {
const cookies = parse(document.cookie);
if (!init || Object.keys(init).length === 0) {
if (init == null || Object.keys(init).length === 0) {
return cookies;
}
if (init == null) {
throw new TypeError('CookieStoreGetOptions must not be empty');
} else if (init instanceof Object && !Object.keys(init).length) {
throw new TypeError('CookieStoreGetOptions must not be empty');
}
let name: string | undefined;
let url;
if (typeof init === 'string') {
Expand Down Expand Up @@ -298,18 +309,100 @@ class CookieStore extends EventTarget {
}
}

if (!window.cookieStore) {
window.CookieStore = CookieStore;
window.cookieStore = Object.create(CookieStore.prototype);
window.CookieChangeEvent = CookieChangeEvent;
interface CookieStoreGetOptions {
name?: string;
url?: string;
}

const workerSubscriptions = new WeakMap<
CookieStoreManager,
CookieStoreGetOptions[]
>();

const registrations = new WeakMap<
CookieStoreManager,
ServiceWorkerRegistration
>();

class CookieStoreManager {
get [Symbol.toStringTag]() {
return 'CookieStoreManager';
}

constructor() {
throw new TypeError('Illegal Constructor');
}

async subscribe(subscriptions: CookieStoreGetOptions[]): Promise<void> {
const currentSubcriptions = workerSubscriptions.get(this) || [];
const worker = registrations.get(this);
if (!worker) throw new TypeError('Illegal invocation');
for (const subscription of subscriptions) {
const name = subscription.name;
const url = new URL(subscription.url || '', worker.scope).toString();

if (currentSubcriptions.some((x) => x.name === name && x.url === url))
continue;
currentSubcriptions.push({
name: subscription.name,
url,
});
}
workerSubscriptions.set(this, currentSubcriptions);
}

async getSubscriptions(): Promise<CookieStoreGetOptions[]> {
return (workerSubscriptions.get(this) || []).map(({ name, url }) => ({
name,
url,
}));
}

async unsubscribe(subscriptions: CookieStoreGetOptions[]): Promise<void> {
let currentSubcriptions = workerSubscriptions.get(this) || [];

const worker = registrations.get(this);
if (!worker) throw new TypeError('Illegal invocation');

for (const subscription of subscriptions) {
const name = subscription.name;
// TODO: Parse the url with the relevant settings objects API base URL.
// https://wicg.github.io/cookie-store/#CookieStoreManager-unsubscribe
const url = new URL(subscription.url || '', worker.scope).toString();
currentSubcriptions = currentSubcriptions.filter((x) => {
if (x.name !== name) return true;
if (x.url !== url) return true;
return false;
});
}
workerSubscriptions.set(this, currentSubcriptions);
}
}

if (!ServiceWorkerRegistration.prototype.cookies) {
Object.defineProperty(ServiceWorkerRegistration.prototype, 'cookies', {
configurable: true,
enumerable: true,
get() {
const manager = Object.create(CookieStoreManager.prototype);
registrations.set(manager, this);
Object.defineProperty(this, 'cookies', { value: manager });
return manager;
},
});
}

declare global {
interface Window {
CookieStore: typeof CookieStore;
cookieStore: CookieStore;
CookieChangeEvent: typeof CookieChangeEvent;
CookieStoreManager: typeof CookieStoreManager;
}
interface ServiceWorkerRegistration {
cookies: CookieStoreManager;
}
}

export {};
const cookieStore = Object.create(CookieStore.prototype);
export { cookieStore, CookieStore, CookieChangeEvent };
6 changes: 6 additions & 0 deletions test/index.tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* global expect */

import {cookieStore, CookieStore, CookieChangeEvent} from '../dist/index.js'

window.cookieStore = cookieStore
window.CookieStore = CookieStore
window.CookieChangeEvent = CookieChangeEvent

describe('Cookie Store', () => {
beforeEach(() => {
Object.defineProperty(document, 'cookie', {
Expand Down
19 changes: 14 additions & 5 deletions test/karma.conf.cjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
module.exports = function (config) {
config.set({
basePath: '..',
files: [
// Include the compiled library
{ pattern: '../dist/index.js', type: 'module' },
{ pattern: './dist/index.js', type: 'module', included: false },
// Include the compiled service worker polyfill
{ pattern: './dist/service-worker.js', included: false },
// Set up test environment to be able to run WPT tests
{ pattern: './wpt-setup/*.js', type: 'module' },
{ pattern: './test/wpt-setup/*.js', type: 'module' },
// Our tests
{ pattern: './index.tests.js', type: 'module' },
{ pattern: './test/index.tests.js', type: 'module' },
// Web Platform Tests
{ pattern: './wpt/*.js', type: 'module' },
{ pattern: './test/wpt/*.js', type: 'module' },
// Resources
{ pattern: './test/resources/*', included: false },
],
plugins: ['karma-*'],
reporters: ['progress'],
Expand All @@ -19,6 +24,10 @@ module.exports = function (config) {
browsers: ['FirefoxHeadless'],
concurrency: Infinity,
hostname: 'foo.bar.localhost',
urlRoot: '/test',
urlRoot: '/cookie-store/',
singleRun: true,
proxies: {
'/cookie-store/resources/': '/base/test/resources/',
},
});
};
1 change: 1 addition & 0 deletions test/resources/empty_sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Empty service worker
Loading