Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
189 changes: 189 additions & 0 deletions __tests__/models/cookies_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,41 @@

import Hellotext from '../../src/hellotext'
import { Cookies } from '../../src/models'
import { Page } from '../../src/models/page'

beforeEach(() => {
document.cookie = ''
jest.clearAllMocks()

// Mock window.location
delete window.location
window.location = {
protocol: 'https:',
hostname: 'www.example.com',
href: 'https://www.example.com/page'
}
})

describe('set', () => {
it('sets the value of a cookie', () => {
// Mock document.cookie to simulate actual browser behavior
let cookieStore = {}

Object.defineProperty(document, 'cookie', {
get: function() {
return Object.entries(cookieStore)
.map(([key, value]) => `${key}=${value}`)
.join('; ')
},
set: function(cookieString) {
const match = cookieString.match(/^([^=]+)=([^;]+)/)
if (match) {
cookieStore[match[1]] = match[2]
}
},
configurable: true
})

expect(Cookies.get('hello_session')).toEqual(undefined)

Cookies.set('hello_session', 'session')
Expand Down Expand Up @@ -98,6 +125,24 @@ describe('set', () => {
})

it('sets cookie before dispatching event (even if dispatch fails)', () => {
// Mock document.cookie to simulate actual browser behavior
let cookieStore = {}

Object.defineProperty(document, 'cookie', {
get: function() {
return Object.entries(cookieStore)
.map(([key, value]) => `${key}=${value}`)
.join('; ')
},
set: function(cookieString) {
const match = cookieString.match(/^([^=]+)=([^;]+)/)
if (match) {
cookieStore[match[1]] = match[2]
}
},
configurable: true
})

Hellotext.eventEmitter.dispatch.mockImplementation(() => {
throw new Error('Dispatch failed')
})
Expand Down Expand Up @@ -138,3 +183,147 @@ describe('get', () => {
expect(Cookies.get('hello_session')).toEqual('session')
})
})

describe('cookie attributes', () => {
let setCookieSpy

beforeEach(() => {
// Spy on document.cookie setter
setCookieSpy = jest.spyOn(document, 'cookie', 'set')
})

afterEach(() => {
setCookieSpy.mockRestore()
})

describe('Secure flag', () => {
it('sets Secure flag when protocol is https', () => {
window.location.protocol = 'https:'

Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toContain('Secure')
})

it('does not set Secure flag when protocol is http', () => {
window.location.protocol = 'http:'

Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).not.toContain('Secure')
})
})

describe('domain attribute', () => {
it('sets domain attribute for regular TLD', () => {
window.location.hostname = 'www.example.com'

Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toContain('domain=.example.com')
})

it('sets domain attribute for multi-part TLD', () => {
window.location.hostname = 'secure.storename.com.br'

Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toContain('domain=.storename.com.br')
})

it('sets domain attribute for VTEX domains', () => {
window.location.hostname = 'storename.vtexcommercestable.com.br'

Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toContain('domain=.storename.vtexcommercestable.com.br')
})

it('does not set domain attribute when getRootDomain returns null', () => {
jest.spyOn(Page, 'getRootDomain').mockReturnValue(null)

Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).not.toMatch(/domain=/)

Page.getRootDomain.mockRestore()
})

it('handles localhost without subdomain prefix', () => {
window.location.hostname = 'localhost'

Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toContain('domain=localhost')
})
})

describe('SameSite attribute', () => {
it('sets SameSite=Lax attribute', () => {
Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toContain('SameSite=Lax')
})
})

describe('max-age attribute', () => {
it('sets max-age to 10 years', () => {
Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
const tenYearsInSeconds = 10 * 365 * 24 * 60 * 60
expect(cookieString).toContain(`max-age=${tenYearsInSeconds}`)
})
})

describe('path attribute', () => {
it('sets path to /', () => {
Cookies.set('test_cookie', 'value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toContain('path=/')
})
})

describe('complete cookie string format', () => {
it('formats cookie with all attributes when domain is available (https)', () => {
window.location.protocol = 'https:'
window.location.hostname = 'www.example.com'

Cookies.set('hello_session', 'test-value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toBe('hello_session=test-value; path=/; Secure; domain=.example.com; max-age=315360000; SameSite=Lax')
})

it('formats cookie without domain attribute when domain is null (https)', () => {
window.location.protocol = 'https:'
jest.spyOn(Page, 'getRootDomain').mockReturnValue(null)

Cookies.set('hello_session', 'test-value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toBe('hello_session=test-value; path=/; Secure; max-age=315360000; SameSite=Lax')

Page.getRootDomain.mockRestore()
})

it('formats cookie without Secure flag on http', () => {
window.location.protocol = 'http:'
window.location.hostname = 'localhost'

Cookies.set('hello_session', 'test-value')

const cookieString = setCookieSpy.mock.calls[0][0]
expect(cookieString).toBe('hello_session=test-value; path=/; domain=localhost; max-age=315360000; SameSite=Lax')
})
})
})
112 changes: 112 additions & 0 deletions __tests__/models/page_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,118 @@ describe('Page', () => {
})
})

describe('domain getter', () => {
it('returns root domain with leading dot for regular TLDs', () => {
const page = new Page('https://www.example.com/path')
expect(page.domain).toBe('.example.com')
})

it('handles subdomains correctly for regular TLDs', () => {
const page = new Page('https://secure.checkout.example.com/cart')
expect(page.domain).toBe('.example.com')
})

it('returns root domain for multi-part TLDs like .com.br', () => {
const page = new Page('https://secure.storename.com.br/checkout')
expect(page.domain).toBe('.storename.com.br')
})

it('handles VTEX domains correctly', () => {
const page = new Page('https://storename.vtexcommercestable.com.br/products')
expect(page.domain).toBe('.storename.vtexcommercestable.com.br')
})

it('handles Shopify domains correctly', () => {
const page = new Page('https://mystore.myshopify.com/products')
expect(page.domain).toBe('.mystore.myshopify.com')
})

it('handles VTEX myvtex domains correctly', () => {
const page = new Page('https://storename.myvtex.com/products')
expect(page.domain).toBe('.storename.myvtex.com')
})

it('handles VTEX domains with subdomains', () => {
const page = new Page('https://secure.storename.vtexcommercestable.com.br/checkout')
expect(page.domain).toBe('.storename.vtexcommercestable.com.br')
})

it('handles Shopify domains with subdomains', () => {
const page = new Page('https://checkout.mystore.myshopify.com/cart')
expect(page.domain).toBe('.mystore.myshopify.com')
})

it('handles VTEX myvtex domains with subdomains', () => {
const page = new Page('https://admin.storename.myvtex.com/admin')
expect(page.domain).toBe('.storename.myvtex.com')
})

it('handles Wix domains correctly', () => {
const page = new Page('https://mystore.wixsite.com/store')
expect(page.domain).toBe('.mystore.wixsite.com')
})

it('handles Wix domains with subdomains', () => {
const page = new Page('https://checkout.mystore.wixsite.com/store')
expect(page.domain).toBe('.mystore.wixsite.com')
})

it('does not confuse regular domains containing platform keywords', () => {
// A regular domain that happens to contain "myshopify" in its name
const page = new Page('https://www.notmyshopify.com/page')
expect(page.domain).toBe('.notmyshopify.com')
})

it('returns root domain for .co.uk domains', () => {
const page = new Page('https://www.example.co.uk/page')
expect(page.domain).toBe('.example.co.uk')
})

it('handles localhost without leading dot', () => {
const page = new Page('http://localhost:3000/test')
expect(page.domain).toBe('localhost')
})

it('handles single-part domains without leading dot', () => {
const page = new Page('http://myserver/path')
expect(page.domain).toBe('myserver')
})

it('handles bare domain without subdomain for regular TLD', () => {
const page = new Page('https://example.com/')
expect(page.domain).toBe('.example.com')
})

it('handles bare domain without subdomain for multi-part TLD', () => {
const page = new Page('https://example.com.br/')
expect(page.domain).toBe('.example.com.br')
})

it('returns domain for default window.location', () => {
const page = new Page()
expect(page.domain).toBe('.example.com')
})

it('returns null for invalid URL', () => {
const page = new Page('not-a-valid-url')
expect(page.domain).toBeNull()
})

it('returns null when URL is undefined', () => {
// Mock window.location to return undefined
delete window.location
window.location = { href: undefined }

const page = new Page()
expect(page.domain).toBeNull()
})

it('returns null when URL is empty string', () => {
const page = new Page('')
expect(page.domain).toBeNull()
})
})

describe('dynamic behavior', () => {
it('reflects changes in window.location for default page', () => {
const page = new Page()
Expand Down
2 changes: 1 addition & 1 deletion dist/hellotext.js

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion lib/models/cookies.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.Cookies = void 0;
var _hellotext = _interopRequireDefault(require("../hellotext"));
var _page = require("./page");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
Expand All @@ -19,7 +20,15 @@ let Cookies = /*#__PURE__*/function () {
key: "set",
value: function set(name, value) {
if (typeof document !== 'undefined') {
document.cookie = `${name}=${value}; path=/;`;
const secure = window.location.protocol === 'https:' ? '; Secure' : '';
const domain = _page.Page.getRootDomain();
const maxAge = 10 * 365 * 24 * 60 * 60; // 10 years in seconds

if (domain) {
document.cookie = `${name}=${value}; path=/${secure}; domain=${domain}; max-age=${maxAge}; SameSite=Lax`;
} else {
document.cookie = `${name}=${value}; path=/${secure}; max-age=${maxAge}; SameSite=Lax`;
}
}
if (name === 'hello_session') {
_hellotext.default.eventEmitter.dispatch('session-set', value);
Expand Down
11 changes: 10 additions & 1 deletion lib/models/cookies.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _d
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
import Hellotext from '../hellotext';
import { Page } from './page';
var Cookies = /*#__PURE__*/function () {
function Cookies() {
_classCallCheck(this, Cookies);
Expand All @@ -12,7 +13,15 @@ var Cookies = /*#__PURE__*/function () {
key: "set",
value: function set(name, value) {
if (typeof document !== 'undefined') {
document.cookie = "".concat(name, "=").concat(value, "; path=/;");
var secure = window.location.protocol === 'https:' ? '; Secure' : '';
var domain = Page.getRootDomain();
var maxAge = 10 * 365 * 24 * 60 * 60; // 10 years in seconds

if (domain) {
document.cookie = "".concat(name, "=").concat(value, "; path=/").concat(secure, "; domain=").concat(domain, "; max-age=").concat(maxAge, "; SameSite=Lax");
} else {
document.cookie = "".concat(name, "=").concat(value, "; path=/").concat(secure, "; max-age=").concat(maxAge, "; SameSite=Lax");
}
}
if (name === 'hello_session') {
Hellotext.eventEmitter.dispatch('session-set', value);
Expand Down
Loading