Skip to content

Commit d7ac23c

Browse files
committed
Added test for JS library
1 parent 39680fb commit d7ac23c

File tree

8 files changed

+339
-12
lines changed

8 files changed

+339
-12
lines changed

crates/js/lib/src/core/render.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { log } from './log';
22
import type { AdUnit } from './types';
33
import { getUnit, getAllUnits, firstSize } from './registry';
4+
import NORMALIZE_CSS from './styles/normalize.css?inline';
5+
import IFRAME_TEMPLATE from './templates/iframe.html?raw';
46

57
function normalizeId(raw: string): string {
68
const s = String(raw ?? '').trim();
@@ -101,10 +103,6 @@ export function renderCreativeIntoSlot(slotId: string, html: string): void {
101103
}
102104
}
103105

104-
// Minimal normalize CSS to reset default margins and typography inside the iframe
105-
const NORMALIZE_CSS =
106-
'/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}';
107-
108106
type IframeOptions = { name?: string; title?: string; width?: number; height?: number };
109107

110108
export function createAdIframe(
@@ -154,12 +152,18 @@ function writeHtmlToIframe(iframe: HTMLIFrameElement, creativeHtml: string): voi
154152
try {
155153
const doc = (iframe.contentDocument || iframe.contentWindow?.document) as Document | undefined;
156154
if (!doc) return;
157-
// Build full HTML with normalize CSS to avoid default body margins
158-
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>${NORMALIZE_CSS}</style></head><body style="margin:0;padding:0;overflow:hidden">${creativeHtml}</body></html>`;
155+
const html = buildIframeDocument(creativeHtml);
159156
doc.open();
160157
doc.write(html);
161158
doc.close();
162159
} catch (err) {
163160
log.warn('renderCreativeIntoSlot: iframe write failed', { err });
164161
}
165162
}
163+
164+
function buildIframeDocument(creativeHtml: string): string {
165+
return IFRAME_TEMPLATE.replace('%NORMALIZE_CSS%', NORMALIZE_CSS).replace(
166+
'%CREATIVE_HTML%',
167+
creativeHtml
168+
);
169+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2+
button,
3+
hr,
4+
input {
5+
overflow: visible;
6+
}
7+
8+
progress,
9+
sub,
10+
sup {
11+
vertical-align: baseline;
12+
}
13+
14+
[type='checkbox'],
15+
[type='radio'],
16+
legend {
17+
box-sizing: border-box;
18+
padding: 0;
19+
}
20+
21+
html {
22+
line-height: 1.15;
23+
-webkit-text-size-adjust: 100%;
24+
}
25+
26+
body {
27+
margin: 0;
28+
}
29+
30+
details,
31+
main {
32+
display: block;
33+
}
34+
35+
h1 {
36+
font-size: 2em;
37+
margin: 0.67em 0;
38+
}
39+
40+
hr {
41+
box-sizing: content-box;
42+
height: 0;
43+
}
44+
45+
code,
46+
kbd,
47+
pre,
48+
samp {
49+
font-family: monospace, monospace;
50+
font-size: 1em;
51+
}
52+
53+
a {
54+
background-color: transparent;
55+
}
56+
57+
abbr[title] {
58+
border-bottom: none;
59+
text-decoration: underline dotted;
60+
}
61+
62+
b,
63+
strong {
64+
font-weight: bolder;
65+
}
66+
67+
small {
68+
font-size: 80%;
69+
}
70+
71+
sub,
72+
sup {
73+
font-size: 75%;
74+
line-height: 0;
75+
position: relative;
76+
}
77+
78+
sub {
79+
bottom: -0.25em;
80+
}
81+
82+
sup {
83+
top: -0.5em;
84+
}
85+
86+
img {
87+
border-style: none;
88+
}
89+
90+
button,
91+
input,
92+
optgroup,
93+
select,
94+
textarea {
95+
font-family: inherit;
96+
font-size: 100%;
97+
line-height: 1.15;
98+
margin: 0;
99+
}
100+
101+
button,
102+
select {
103+
text-transform: none;
104+
}
105+
106+
[type='button'],
107+
[type='reset'],
108+
[type='submit'],
109+
button {
110+
-webkit-appearance: button;
111+
}
112+
113+
[type='button']::-moz-focus-inner,
114+
[type='reset']::-moz-focus-inner,
115+
[type='submit']::-moz-focus-inner,
116+
button::-moz-focus-inner {
117+
border-style: none;
118+
padding: 0;
119+
}
120+
121+
[type='button']:-moz-focusring,
122+
[type='reset']:-moz-focusring,
123+
[type='submit']:-moz-focusring,
124+
button:-moz-focusring {
125+
outline: 1px dotted ButtonText;
126+
}
127+
128+
fieldset {
129+
padding: 0.35em 0.75em 0.625em;
130+
}
131+
132+
legend {
133+
color: inherit;
134+
display: table;
135+
max-width: 100%;
136+
white-space: normal;
137+
}
138+
139+
textarea {
140+
overflow: auto;
141+
}
142+
143+
[type='number']::-webkit-inner-spin-button,
144+
[type='number']::-webkit-outer-spin-button {
145+
height: auto;
146+
}
147+
148+
[type='search'] {
149+
-webkit-appearance: textfield;
150+
outline-offset: -2px;
151+
}
152+
153+
[type='search']::-webkit-search-decoration {
154+
-webkit-appearance: none;
155+
}
156+
157+
::-webkit-file-upload-button {
158+
-webkit-appearance: button;
159+
font: inherit;
160+
}
161+
162+
summary {
163+
display: list-item;
164+
}
165+
166+
[hidden],
167+
template {
168+
display: none;
169+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<style>%NORMALIZE_CSS%</style>
6+
</head>
7+
<body style="margin:0;padding:0;overflow:hidden">%CREATIVE_HTML%</body>
8+
</html>
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2-
import '../src/core/index';
2+
import '../../src/core/index';
33

44
declare global {
55
interface Window {
@@ -63,8 +63,8 @@ describe('tsjs', () => {
6363
window.pbjs.requestBids({ bidsBackHandler: () => {} });
6464
});
6565
vi.resetModules();
66-
await import('../src/core/index');
67-
await import('../src/ext/index');
66+
await import('../../src/core/index');
67+
await import('../../src/ext/index');
6868

6969
expect(window.tsjs).toBe(window.pbjs);
7070
const el = document.getElementById('pbslot');
@@ -75,7 +75,7 @@ describe('tsjs', () => {
7575
it('requestBids invokes callback and renders', () => {
7676
// Ensure prebid extension is installed
7777
// eslint-disable-next-line @typescript-eslint/no-floating-promises
78-
import('../src/ext/index');
78+
import('../../src/ext/index');
7979
let called = false;
8080
window.tsjs.setConfig({ mode: 'thirdParty' } as any);
8181
window.tsjs.addAdUnits({ code: 'rb', mediaTypes: { banner: { sizes: [[320, 50]] } } });
@@ -96,7 +96,7 @@ describe('tsjs', () => {
9696
window.tsjs.renderAllAdUnits();
9797
});
9898
vi.resetModules();
99-
await import('../src/core/index');
99+
await import('../../src/core/index');
100100
const el = document.getElementById('qslot');
101101
expect(el).toBeTruthy();
102102
expect(el!.textContent).toContain('300x250');
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const PROXY_RESPONSE =
1010

1111
async function importCreativeModule() {
1212
delete (globalThis as any).__ts_creative_installed;
13-
await import('../src/creative/index');
13+
await import('../../src/creative/index');
1414
}
1515

1616
describe('tsjs creative guard', () => {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
3+
import installPrebidJsShim from '../../src/ext/prebidjs';
4+
5+
const ORIGINAL_WINDOW = global.window;
6+
7+
function createWindow() {
8+
return {
9+
tsjs: undefined as any,
10+
pbjs: undefined as any,
11+
} as Window & { tsjs?: any; pbjs?: any };
12+
}
13+
14+
describe('ext/prebidjs', () => {
15+
let testWindow: ReturnType<typeof createWindow>;
16+
17+
beforeEach(() => {
18+
testWindow = createWindow();
19+
Object.assign(globalThis as any, { window: testWindow });
20+
});
21+
22+
afterEach(() => {
23+
Object.assign(globalThis as any, { window: ORIGINAL_WINDOW });
24+
vi.restoreAllMocks();
25+
});
26+
27+
it('installs shim, aliases pbjs to tsjs, and shares queue', () => {
28+
const result = installPrebidJsShim();
29+
expect(result).toBe(true);
30+
expect(testWindow.pbjs).toBe(testWindow.tsjs);
31+
expect(Array.isArray(testWindow.tsjs!.que)).toBe(true);
32+
});
33+
34+
it('flushes queued pbjs callbacks', () => {
35+
const callback = vi.fn();
36+
testWindow.pbjs = { que: [callback] } as any;
37+
38+
installPrebidJsShim();
39+
40+
expect(callback).toHaveBeenCalled();
41+
expect(testWindow.pbjs).toBe(testWindow.tsjs);
42+
});
43+
44+
it('ensures shared queue and requestBids shim delegates to requestAds', () => {
45+
installPrebidJsShim();
46+
47+
const api = testWindow.tsjs!;
48+
api.requestAds = vi.fn();
49+
const requestBids = testWindow.pbjs!.requestBids.bind(testWindow.pbjs);
50+
51+
const callback = vi.fn();
52+
requestBids(callback);
53+
expect(api.requestAds).toHaveBeenCalledWith(callback, undefined);
54+
55+
requestBids({ timeout: 100 } as any);
56+
expect(api.requestAds).toHaveBeenCalledWith({ timeout: 100 });
57+
});
58+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
3+
import { delay, queueTask } from '../../src/shared/async';
4+
5+
describe('shared/async', () => {
6+
it('queueTask uses queueMicrotask when available', async () => {
7+
const microtaskSpy = vi.fn();
8+
const originalQueue = global.queueMicrotask;
9+
10+
global.queueMicrotask = (cb: () => void) => {
11+
microtaskSpy();
12+
cb();
13+
};
14+
15+
const callback = vi.fn();
16+
queueTask(callback);
17+
await Promise.resolve();
18+
19+
expect(microtaskSpy).toHaveBeenCalled();
20+
expect(callback).toHaveBeenCalled();
21+
22+
global.queueMicrotask = originalQueue;
23+
});
24+
25+
it('queueTask falls back to setTimeout and delay resolves after given time', async () => {
26+
vi.useFakeTimers();
27+
const callback = vi.fn();
28+
queueTask(callback);
29+
expect(callback).not.toHaveBeenCalled();
30+
31+
const resolver = vi.fn();
32+
const promise = delay(50).then(resolver);
33+
expect(callback).not.toHaveBeenCalled();
34+
expect(resolver).not.toHaveBeenCalled();
35+
36+
vi.advanceTimersByTime(50);
37+
await promise;
38+
expect(callback).toHaveBeenCalled();
39+
expect(resolver).toHaveBeenCalled();
40+
41+
vi.useRealTimers();
42+
});
43+
});

0 commit comments

Comments
 (0)