mirror of
https://github.com/lancedikson/bowser
synced 2026-02-09 17:40:09 +00:00
feat: add clientHints support + DuckDuckGo detection (#595)
This commit is contained in:
parent
0c6fdd7248
commit
aaa67db25e
48
index.d.ts
vendored
48
index.d.ts
vendored
@ -7,20 +7,36 @@ export as namespace Bowser;
|
|||||||
|
|
||||||
declare namespace Bowser {
|
declare namespace Bowser {
|
||||||
/**
|
/**
|
||||||
* Creates a Parser instance
|
* User-Agent Client Hints data structure
|
||||||
* @param {string} UA - User agent string
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData
|
||||||
* @param {boolean} skipParsing
|
|
||||||
*/
|
*/
|
||||||
|
interface ClientHints {
|
||||||
|
brands?: Array<{ brand: string; version: string }>;
|
||||||
|
mobile?: boolean;
|
||||||
|
platform?: string;
|
||||||
|
platformVersion?: string;
|
||||||
|
architecture?: string;
|
||||||
|
model?: string;
|
||||||
|
wow64?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Parser instance
|
||||||
|
* @param {string} UA - User agent string
|
||||||
|
* @param {boolean | ClientHints} skipParsingOrHints - Either skip parsing flag or Client Hints
|
||||||
|
* @param {ClientHints} clientHints - User-Agent Client Hints data
|
||||||
|
*/
|
||||||
function getParser(UA: string, skipParsing?: boolean): Parser.Parser;
|
function getParser(UA: string, skipParsing?: boolean): Parser.Parser;
|
||||||
|
function getParser(UA: string, clientHints?: ClientHints): Parser.Parser;
|
||||||
|
function getParser(UA: string, skipParsing?: boolean, clientHints?: ClientHints): Parser.Parser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Parser instance and runs Parser.getResult immediately
|
* Creates a Parser instance and runs Parser.getResult immediately
|
||||||
* @param UA - User agent string
|
* @param UA - User agent string
|
||||||
|
* @param clientHints - User-Agent Client Hints data
|
||||||
* @returns {Parser.ParsedResult}
|
* @returns {Parser.ParsedResult}
|
||||||
*/
|
*/
|
||||||
|
function parse(UA: string, clientHints?: ClientHints): Parser.ParsedResult;
|
||||||
function parse(UA: string): Parser.ParsedResult;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants exposed via bowser getters
|
* Constants exposed via bowser getters
|
||||||
@ -33,6 +49,28 @@ declare namespace Bowser {
|
|||||||
namespace Parser {
|
namespace Parser {
|
||||||
interface Parser {
|
interface Parser {
|
||||||
constructor(UA: string, skipParsing?: boolean): Parser.Parser;
|
constructor(UA: string, skipParsing?: boolean): Parser.Parser;
|
||||||
|
constructor(UA: string, clientHints?: ClientHints): Parser.Parser;
|
||||||
|
constructor(UA: string, skipParsing?: boolean, clientHints?: ClientHints): Parser.Parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Client Hints data
|
||||||
|
* @return {ClientHints|null}
|
||||||
|
*/
|
||||||
|
getHints(): ClientHints | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a brand exists in Client Hints brands array
|
||||||
|
* @param {string} brandName The brand name to check for
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
hasBrand(brandName: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get brand version from Client Hints
|
||||||
|
* @param {string} brandName The brand name to get version for
|
||||||
|
* @return {string|undefined}
|
||||||
|
*/
|
||||||
|
getBrandVersion(brandName: string): string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the version is equals the browser version
|
* Check if the version is equals the browser version
|
||||||
|
|||||||
@ -28,33 +28,49 @@ class Bowser {
|
|||||||
* Creates a {@link Parser} instance
|
* Creates a {@link Parser} instance
|
||||||
*
|
*
|
||||||
* @param {String} UA UserAgent string
|
* @param {String} UA UserAgent string
|
||||||
* @param {Boolean} [skipParsing=false] Will make the Parser postpone parsing until you ask it
|
* @param {Boolean|Object} [skipParsingOrHints=false] Either a boolean to skip parsing,
|
||||||
* explicitly. Same as `skipParsing` for {@link Parser}.
|
* or a ClientHints object (navigator.userAgentData)
|
||||||
|
* @param {Object} [clientHints] User-Agent Client Hints data (navigator.userAgentData)
|
||||||
* @returns {Parser}
|
* @returns {Parser}
|
||||||
* @throws {Error} when UA is not a String
|
* @throws {Error} when UA is not a String
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const parser = Bowser.getParser(window.navigator.userAgent);
|
* const parser = Bowser.getParser(window.navigator.userAgent);
|
||||||
* const result = parser.getResult();
|
* const result = parser.getResult();
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // With User-Agent Client Hints
|
||||||
|
* const parser = Bowser.getParser(
|
||||||
|
* window.navigator.userAgent,
|
||||||
|
* window.navigator.userAgentData
|
||||||
|
* );
|
||||||
*/
|
*/
|
||||||
static getParser(UA, skipParsing = false) {
|
static getParser(UA, skipParsingOrHints = false, clientHints = null) {
|
||||||
if (typeof UA !== 'string') {
|
if (typeof UA !== 'string') {
|
||||||
throw new Error('UserAgent should be a string');
|
throw new Error('UserAgent should be a string');
|
||||||
}
|
}
|
||||||
return new Parser(UA, skipParsing);
|
return new Parser(UA, skipParsingOrHints, clientHints);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a {@link Parser} instance and runs {@link Parser.getResult} immediately
|
* Creates a {@link Parser} instance and runs {@link Parser.getResult} immediately
|
||||||
*
|
*
|
||||||
* @param UA
|
* @param {String} UA UserAgent string
|
||||||
|
* @param {Object} [clientHints] User-Agent Client Hints data (navigator.userAgentData)
|
||||||
* @return {ParsedResult}
|
* @return {ParsedResult}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const result = Bowser.parse(window.navigator.userAgent);
|
* const result = Bowser.parse(window.navigator.userAgent);
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // With User-Agent Client Hints
|
||||||
|
* const result = Bowser.parse(
|
||||||
|
* window.navigator.userAgent,
|
||||||
|
* window.navigator.userAgentData
|
||||||
|
* );
|
||||||
*/
|
*/
|
||||||
static parse(UA) {
|
static parse(UA, clientHints = null) {
|
||||||
return (new Parser(UA)).getResult();
|
return (new Parser(UA, clientHints)).getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get BROWSER_MAP() {
|
static get BROWSER_MAP() {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export const BROWSER_ALIASES_MAP = {
|
|||||||
Chromium: 'chromium',
|
Chromium: 'chromium',
|
||||||
Diffbot: 'diffbot',
|
Diffbot: 'diffbot',
|
||||||
DuckDuckBot: 'duckduckbot',
|
DuckDuckBot: 'duckduckbot',
|
||||||
|
DuckDuckGo: 'duckduckgo',
|
||||||
Electron: 'electron',
|
Electron: 'electron',
|
||||||
Epiphany: 'epiphany',
|
Epiphany: 'epiphany',
|
||||||
FacebookExternalHit: 'facebookexternalhit',
|
FacebookExternalHit: 'facebookexternalhit',
|
||||||
@ -82,6 +83,7 @@ export const BROWSER_MAP = {
|
|||||||
chromium: 'Chromium',
|
chromium: 'Chromium',
|
||||||
diffbot: 'Diffbot',
|
diffbot: 'Diffbot',
|
||||||
duckduckbot: 'DuckDuckBot',
|
duckduckbot: 'DuckDuckBot',
|
||||||
|
duckduckgo: 'DuckDuckGo',
|
||||||
edge: 'Microsoft Edge',
|
edge: 'Microsoft Edge',
|
||||||
electron: 'Electron',
|
electron: 'Electron',
|
||||||
epiphany: 'Epiphany',
|
epiphany: 'Epiphany',
|
||||||
|
|||||||
@ -987,6 +987,39 @@ const browsersList = [
|
|||||||
return browser;
|
return browser;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
/* DuckDuckGo Browser */
|
||||||
|
{
|
||||||
|
test(parser) {
|
||||||
|
// Chromium platforms (Android, Windows): check Client Hints brands first
|
||||||
|
if (parser.hasBrand('DuckDuckGo')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// WebKit platforms (iOS, macOS): check UA string for Ddg/version suffix
|
||||||
|
return parser.test(/\sDdg\/[\d.]+$/i);
|
||||||
|
},
|
||||||
|
describe(ua, parser) {
|
||||||
|
const browser = {
|
||||||
|
name: 'DuckDuckGo',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try Client Hints brand version first
|
||||||
|
if (parser) {
|
||||||
|
const hintsVersion = parser.getBrandVersion('DuckDuckGo');
|
||||||
|
if (hintsVersion) {
|
||||||
|
browser.version = hintsVersion;
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to WebKit UA pattern
|
||||||
|
const uaVersion = Utils.getFirstMatch(/\sDdg\/([\d.]+)$/i, ua);
|
||||||
|
if (uaVersion) {
|
||||||
|
browser.version = uaVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return browser;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: [/chromium/i],
|
test: [/chromium/i],
|
||||||
describe(ua) {
|
describe(ua) {
|
||||||
|
|||||||
@ -4,6 +4,17 @@ import platformParsersList from './parser-platforms.js';
|
|||||||
import enginesParsersList from './parser-engines.js';
|
import enginesParsersList from './parser-engines.js';
|
||||||
import Utils from './utils.js';
|
import Utils from './utils.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ClientHints
|
||||||
|
* @property {Array<{brand: string, version: string}>} [brands] Array of brand objects
|
||||||
|
* @property {boolean} [mobile] Whether the device is mobile
|
||||||
|
* @property {string} [platform] Platform name (e.g., "Windows", "macOS")
|
||||||
|
* @property {string} [platformVersion] Platform version
|
||||||
|
* @property {string} [architecture] CPU architecture
|
||||||
|
* @property {string} [model] Device model
|
||||||
|
* @property {boolean} [wow64] Whether running under WoW64
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main class that arranges the whole parsing process.
|
* The main class that arranges the whole parsing process.
|
||||||
*/
|
*/
|
||||||
@ -12,21 +23,32 @@ class Parser {
|
|||||||
* Create instance of Parser
|
* Create instance of Parser
|
||||||
*
|
*
|
||||||
* @param {String} UA User-Agent string
|
* @param {String} UA User-Agent string
|
||||||
* @param {Boolean} [skipParsing=false] parser can skip parsing in purpose of performance
|
* @param {Boolean|ClientHints} [skipParsingOrHints=false] Either a boolean to skip parsing,
|
||||||
* improvements if you need to make a more particular parsing
|
* or a ClientHints object containing User-Agent Client Hints data
|
||||||
* like {@link Parser#parseBrowser} or {@link Parser#parsePlatform}
|
* @param {ClientHints} [clientHints] User-Agent Client Hints data (navigator.userAgentData)
|
||||||
*
|
*
|
||||||
* @throw {Error} in case of empty UA String
|
* @throw {Error} in case of empty UA String
|
||||||
*
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor(UA, skipParsing = false) {
|
constructor(UA, skipParsingOrHints = false, clientHints = null) {
|
||||||
if (UA === void (0) || UA === null || UA === '') {
|
if (UA === void (0) || UA === null || UA === '') {
|
||||||
throw new Error("UserAgent parameter can't be empty");
|
throw new Error("UserAgent parameter can't be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
this._ua = UA;
|
this._ua = UA;
|
||||||
|
|
||||||
|
// Handle overloaded constructor: (UA, clientHints) or (UA, skipParsing, clientHints)
|
||||||
|
let skipParsing = false;
|
||||||
|
if (typeof skipParsingOrHints === 'boolean') {
|
||||||
|
skipParsing = skipParsingOrHints;
|
||||||
|
this._hints = clientHints;
|
||||||
|
} else if (skipParsingOrHints != null && typeof skipParsingOrHints === 'object') {
|
||||||
|
this._hints = skipParsingOrHints;
|
||||||
|
} else {
|
||||||
|
this._hints = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef ParsedResult
|
* @typedef ParsedResult
|
||||||
* @property {Object} browser
|
* @property {Object} browser
|
||||||
@ -56,6 +78,51 @@ class Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Client Hints data
|
||||||
|
* @return {ClientHints|null}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getHints() {
|
||||||
|
return this._hints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a brand exists in Client Hints brands array
|
||||||
|
* @param {string} brandName The brand name to check for
|
||||||
|
* @return {boolean}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
hasBrand(brandName) {
|
||||||
|
if (!this._hints || !Array.isArray(this._hints.brands)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const brandLower = brandName.toLowerCase();
|
||||||
|
return this._hints.brands.some(
|
||||||
|
b => b.brand && b.brand.toLowerCase() === brandLower,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get brand version from Client Hints
|
||||||
|
* @param {string} brandName The brand name to get version for
|
||||||
|
* @return {string|undefined}
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getBrandVersion(brandName) {
|
||||||
|
if (!this._hints || !Array.isArray(this._hints.brands)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const brandLower = brandName.toLowerCase();
|
||||||
|
const brand = this._hints.brands.find(
|
||||||
|
b => b.brand && b.brand.toLowerCase() === brandLower,
|
||||||
|
);
|
||||||
|
return brand ? brand.version : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get UserAgent string of current Parser instance
|
* Get UserAgent string of current Parser instance
|
||||||
* @return {String} User-Agent String of the current <Parser> object
|
* @return {String} User-Agent String of the current <Parser> object
|
||||||
@ -95,7 +162,7 @@ class Parser {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (browserDescriptor) {
|
if (browserDescriptor) {
|
||||||
this.parsedResult.browser = browserDescriptor.describe(this.getUA());
|
this.parsedResult.browser = browserDescriptor.describe(this.getUA(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.parsedResult.browser;
|
return this.parsedResult.browser;
|
||||||
|
|||||||
@ -2927,6 +2927,55 @@
|
|||||||
type: "bot"
|
type: "bot"
|
||||||
vendor: "DuckDuckGo"
|
vendor: "DuckDuckGo"
|
||||||
engine: {}
|
engine: {}
|
||||||
|
DuckDuckGo:
|
||||||
|
-
|
||||||
|
ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Ddg/7.123.0"
|
||||||
|
spec:
|
||||||
|
browser:
|
||||||
|
name: "DuckDuckGo"
|
||||||
|
version: "7.123.0"
|
||||||
|
os:
|
||||||
|
name: "iOS"
|
||||||
|
version: "18.2"
|
||||||
|
platform:
|
||||||
|
type: "mobile"
|
||||||
|
vendor: "Apple"
|
||||||
|
model: "iPhone"
|
||||||
|
engine:
|
||||||
|
name: "WebKit"
|
||||||
|
version: "605.1.15"
|
||||||
|
-
|
||||||
|
ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Ddg/7.123.0"
|
||||||
|
spec:
|
||||||
|
browser:
|
||||||
|
name: "DuckDuckGo"
|
||||||
|
version: "7.123.0"
|
||||||
|
os:
|
||||||
|
name: "macOS"
|
||||||
|
version: "10.15.7"
|
||||||
|
versionName: "Catalina"
|
||||||
|
platform:
|
||||||
|
type: "desktop"
|
||||||
|
vendor: "Apple"
|
||||||
|
engine:
|
||||||
|
name: "WebKit"
|
||||||
|
version: "605.1.15"
|
||||||
|
-
|
||||||
|
ua: "Mozilla/5.0 (iPad; CPU OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Ddg/7.120.0"
|
||||||
|
spec:
|
||||||
|
browser:
|
||||||
|
name: "DuckDuckGo"
|
||||||
|
version: "7.120.0"
|
||||||
|
os:
|
||||||
|
name: "iOS"
|
||||||
|
version: "17.5"
|
||||||
|
platform:
|
||||||
|
type: "tablet"
|
||||||
|
vendor: "Apple"
|
||||||
|
model: "iPad"
|
||||||
|
engine:
|
||||||
|
name: "WebKit"
|
||||||
|
version: "605.1.15"
|
||||||
InternetArchiveCrawler:
|
InternetArchiveCrawler:
|
||||||
-
|
-
|
||||||
ua: "ia_archiver"
|
ua: "ia_archiver"
|
||||||
|
|||||||
@ -228,3 +228,158 @@ test('Parser.isEngine should pass', (t) => {
|
|||||||
t.is(parser.isEngine('blink'), true);
|
t.is(parser.isEngine('blink'), true);
|
||||||
t.is(parser.isEngine('webkit'), false);
|
t.is(parser.isEngine('webkit'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Client Hints tests
|
||||||
|
const DDG_ANDROID_UA = 'Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.200 Mobile Safari/537.36';
|
||||||
|
const DDG_ANDROID_HINTS = {
|
||||||
|
brands: [
|
||||||
|
{ brand: 'DuckDuckGo', version: '5.225.1' },
|
||||||
|
{ brand: 'Chromium', version: '131' },
|
||||||
|
{ brand: 'Not)A;Brand', version: '99' },
|
||||||
|
],
|
||||||
|
mobile: true,
|
||||||
|
platform: 'Android',
|
||||||
|
platformVersion: '14',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHROME_HINTS = {
|
||||||
|
brands: [
|
||||||
|
{ brand: 'Google Chrome', version: '131' },
|
||||||
|
{ brand: 'Chromium', version: '131' },
|
||||||
|
{ brand: 'Not_A Brand', version: '24' },
|
||||||
|
],
|
||||||
|
mobile: false,
|
||||||
|
platform: 'Windows',
|
||||||
|
platformVersion: '15.0.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EDGE_HINTS = {
|
||||||
|
brands: [
|
||||||
|
{ brand: 'Microsoft Edge', version: '131' },
|
||||||
|
{ brand: 'Chromium', version: '131' },
|
||||||
|
{ brand: 'Not-A.Brand', version: '24' },
|
||||||
|
],
|
||||||
|
mobile: false,
|
||||||
|
platform: 'Windows',
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Parser.getHints returns null when no hints provided', (t) => {
|
||||||
|
const p = new Parser(UA, true);
|
||||||
|
t.is(p.getHints(), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.getHints returns hints when provided via constructor', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.deepEqual(p.getHints(), DDG_ANDROID_HINTS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.getHints with overloaded constructor (UA, hints)', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, DDG_ANDROID_HINTS);
|
||||||
|
t.deepEqual(p.getHints(), DDG_ANDROID_HINTS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.hasBrand returns true for existing brand', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.true(p.hasBrand('DuckDuckGo'));
|
||||||
|
t.true(p.hasBrand('Chromium'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.hasBrand is case insensitive', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.true(p.hasBrand('duckduckgo'));
|
||||||
|
t.true(p.hasBrand('DUCKDUCKGO'));
|
||||||
|
t.true(p.hasBrand('chromium'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.hasBrand returns false for non-existent brand', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.false(p.hasBrand('Firefox'));
|
||||||
|
t.false(p.hasBrand('Safari'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.hasBrand returns false when no hints provided', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, true);
|
||||||
|
t.false(p.hasBrand('DuckDuckGo'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.hasBrand detects GREASE "Not A Brand" variants', (t) => {
|
||||||
|
const p1 = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.true(p1.hasBrand('Not)A;Brand'));
|
||||||
|
|
||||||
|
const p2 = new Parser(DDG_ANDROID_UA, false, CHROME_HINTS);
|
||||||
|
t.true(p2.hasBrand('Not_A Brand'));
|
||||||
|
|
||||||
|
const p3 = new Parser(DDG_ANDROID_UA, false, EDGE_HINTS);
|
||||||
|
t.true(p3.hasBrand('Not-A.Brand'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.getBrandVersion returns version for existing brand', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.is(p.getBrandVersion('DuckDuckGo'), '5.225.1');
|
||||||
|
t.is(p.getBrandVersion('Chromium'), '131');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.getBrandVersion is case insensitive', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.is(p.getBrandVersion('duckduckgo'), '5.225.1');
|
||||||
|
t.is(p.getBrandVersion('CHROMIUM'), '131');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.getBrandVersion returns undefined for non-existent brand', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.is(p.getBrandVersion('Firefox'), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.getBrandVersion returns undefined when no hints provided', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, true);
|
||||||
|
t.is(p.getBrandVersion('DuckDuckGo'), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser.getBrandVersion returns version for GREASE brands', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.is(p.getBrandVersion('Not)A;Brand'), '99');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser detects DuckDuckGo from client hints brands', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, DDG_ANDROID_HINTS);
|
||||||
|
t.is(p.getBrowserName(), 'DuckDuckGo');
|
||||||
|
t.is(p.getBrowserVersion(), '5.225.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser falls back to UA when no client hints provided', (t) => {
|
||||||
|
const p = new Parser(DDG_ANDROID_UA);
|
||||||
|
// Without hints, Chrome is detected from the UA string
|
||||||
|
t.is(p.getBrowserName(), 'Chrome');
|
||||||
|
t.is(p.getBrowserVersion(), '131.0.6778.200');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser with empty brands array falls back to UA parsing', (t) => {
|
||||||
|
const emptyHints = { brands: [], mobile: true, platform: 'Android' };
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, emptyHints);
|
||||||
|
t.false(p.hasBrand('DuckDuckGo'));
|
||||||
|
t.is(p.getBrowserName(), 'Chrome');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser handles malformed hints gracefully', (t) => {
|
||||||
|
const malformedHints = { brands: [{ brand: null }, { version: '1.0' }, {}] };
|
||||||
|
const p = new Parser(DDG_ANDROID_UA, false, malformedHints);
|
||||||
|
t.false(p.hasBrand('DuckDuckGo'));
|
||||||
|
t.is(p.getBrandVersion('anything'), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser with Chrome client hints', (t) => {
|
||||||
|
const chromeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||||
|
const p = new Parser(chromeUA, false, CHROME_HINTS);
|
||||||
|
t.true(p.hasBrand('Google Chrome'));
|
||||||
|
t.true(p.hasBrand('Chromium'));
|
||||||
|
t.true(p.hasBrand('Not_A Brand'));
|
||||||
|
t.is(p.getBrandVersion('Google Chrome'), '131');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Parser with Edge client hints', (t) => {
|
||||||
|
const edgeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0';
|
||||||
|
const p = new Parser(edgeUA, false, EDGE_HINTS);
|
||||||
|
t.true(p.hasBrand('Microsoft Edge'));
|
||||||
|
t.true(p.hasBrand('Chromium'));
|
||||||
|
t.is(p.getBrandVersion('Microsoft Edge'), '131');
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user