Skip to content
This repository was archived by the owner on Apr 27, 2026. It is now read-only.
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
42 changes: 37 additions & 5 deletions acrobat/blocks/rnr/rnr.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,48 @@ function extractMetadata(options) {

// #region IMS Helpers

const getRefreshToken = async () => {
try {
const { tokenInfo } = window.adobeIMS ? await window.adobeIMS.refreshToken() : {};
return tokenInfo;
} catch (e) {
return {
token: null,
error: e,
};
}
};

const attemptTokenRefresh = async () => {
const refreshResult = await getRefreshToken();
if (!refreshResult.error) {
return { token: refreshResult, error: null };
}
return refreshResult;
};

const getImsToken = async (operation) => {
const RETRY_WAIT = 2000;
try {
const token = window.adobeIMS.getAccessToken()?.token;
if (!token) {
throw new Error(`Cannot ${operation} token is missing`);
const accessToken = window.adobeIMS?.getAccessToken();
if (!accessToken || accessToken?.expire?.valueOf() <= Date.now() + (5 * 60 * 1000)) {
const reason = !accessToken ? 'access_token_null' : 'access_token_expired';
const firstAttempt = await attemptTokenRefresh();
if (!firstAttempt.error) return firstAttempt.token?.token;
await new Promise((resolve) => { setTimeout(resolve, RETRY_WAIT); });
const retryAttempt = await attemptTokenRefresh();
if (!retryAttempt.error) return retryAttempt.token?.token;
const errorMsg = `Token refresh failed after retry. refresh_error_${reason}`;
window.lana.log(
`RnR: Cannot ${operation} - ${errorMsg} for verb ${metadata.verb}`,
lanaOptions,
);
return null;
}
return token;
return accessToken?.token;
} catch (error) {
window.lana.log(
`RnR: ${error.message} for verb ${metadata.verb}`,
`RnR: Cannot ${operation} - ${error.message} for verb ${metadata.verb}`,
lanaOptions,
);
return null;
Expand Down
135 changes: 127 additions & 8 deletions test/blocks/rnr/rnr.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ describe('rnr - Ratings and reviews', () => {
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
localStorage.removeItem('rnr-snapshot');
window.mph = { 'rnr-rating-tooltips': 'Poor, Below Average, Good, Very Good, Outstanding' };
window.adobeIMS = { getAccessToken: () => ({ token: 'test-token' }) };
// Mock IMS with valid token that won't expire for 10 minutes
window.adobeIMS = {
getAccessToken: () => ({
token: 'test-token',
expire: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes from now
}),
refreshToken: () => Promise.resolve({
tokenInfo: {
token: 'refreshed-test-token',
expire: new Date(Date.now() + 10 * 60 * 1000),
},
}),
};
window.lana = { log: () => {} };
const rnr = document.querySelector('.rnr');
init(rnr);
Expand All @@ -22,6 +34,9 @@ describe('rnr - Ratings and reviews', () => {
if (window.fetch.restore) window.fetch.restore();
if (window.lana.log.restore) window.lana.log.restore();
if (window.adobeIMS.getAccessToken.restore) window.adobeIMS.getAccessToken.restore();
if (window.adobeIMS.refreshToken && window.adobeIMS.refreshToken.restore) {
window.adobeIMS.refreshToken.restore();
}
});

// #region IMS Token Error Handling
Expand All @@ -39,12 +54,15 @@ describe('rnr - Ratings and reviews', () => {

it('should handle null access token', async () => {
sinon.stub(window.adobeIMS, 'getAccessToken').returns(null);
sinon.stub(window.adobeIMS, 'refreshToken').rejects(new Error('Token refresh failed'));
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const rnr = document.querySelector('.rnr');
await init(rnr);
// Wait for retry logic to complete
await new Promise((resolve) => { setTimeout(resolve, 2500); });
const containerElement = await waitForElement('.rnr-container');
expect(containerElement).to.exist;
});
}).timeout(5000);

it('should handle error thrown by getAccessToken', async () => {
const originalIMS = window.adobeIMS;
Expand All @@ -53,41 +71,142 @@ describe('rnr - Ratings and reviews', () => {
console.error('Intentional test error: IMS not available');
return undefined;
},
refreshToken: () => Promise.reject(new Error('Token refresh failed')),
};
try {
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const rnr = document.querySelector('.rnr');
await init(rnr);
// Wait for retry logic to complete
await new Promise((resolve) => { setTimeout(resolve, 2500); });
const containerElement = await waitForElement('.rnr-container');
expect(containerElement).to.exist;
} finally {
window.adobeIMS = originalIMS;
}
});
}).timeout(5000);

it('should handle token not available when posting review', async () => {
const containerElement = await waitForElement('.rnr-container');
const ratingFieldsetElement = containerElement.querySelector('.rnr-rating-fieldset');
const stars = ratingFieldsetElement.querySelectorAll('input');
window.adobeIMS.getAccessToken = () => null;
window.adobeIMS.refreshToken = () => Promise.reject(new Error('Token refresh failed'));
stars[4].click();
expect(containerElement).to.exist;
});

it('should wait for IMS to be ready', async () => {
const originalGetAccessToken = window.adobeIMS.getAccessToken;
const originalRefreshToken = window.adobeIMS.refreshToken;
let tokenAvailable = false;
window.adobeIMS.getAccessToken = () => (tokenAvailable ? { token: 'test-token' } : null);
let refreshCallCount = 0;
window.adobeIMS.getAccessToken = () => (tokenAvailable ? {
token: 'test-token',
expire: new Date(Date.now() + 10 * 60 * 1000),
} : null);
window.adobeIMS.refreshToken = () => {
refreshCallCount += 1;
// Return success on second refresh attempt (after first retry)
if (refreshCallCount >= 2) {
tokenAvailable = true;
return Promise.resolve({
tokenInfo: {
token: 'refreshed-test-token',
expire: new Date(Date.now() + 10 * 60 * 1000),
},
});
}
return Promise.reject(new Error('Token not ready'));
};
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const rnr = document.querySelector('.rnr');
setTimeout(() => {
tokenAvailable = true;
}, 500);
await init(rnr);
// Wait for retry logic to complete
await new Promise((resolve) => { setTimeout(resolve, 3000); });
const containerElement = await waitForElement('.rnr-container');
expect(containerElement).to.exist;
window.adobeIMS.getAccessToken = originalGetAccessToken;
});
window.adobeIMS.refreshToken = originalRefreshToken;
}).timeout(8000);

it('should successfully refresh expired token', async () => {
const originalIMS = window.adobeIMS;
const refreshTokenStub = sinon.stub().resolves({
tokenInfo: {
token: 'new-refreshed-token',
expire: new Date(Date.now() + 10 * 60 * 1000),
},
});
// Mock expired token
window.adobeIMS = {
getAccessToken: () => ({
token: 'expired-token',
expire: { valueOf: () => Date.now() - 1000 }, // Already expired
}),
refreshToken: refreshTokenStub,
};
const fetchStub = sinon.stub(window, 'fetch').resolves({
ok: true,
json: () => Promise.resolve({
overallRating: 4.5,
ratingHistogram: { rating1: 0, rating2: 0, rating3: 0, rating4: 5, rating5: 5 },
}),
});
try {
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const rnr = document.querySelector('.rnr');
await init(rnr);
// Wait a bit for async operations to complete
await new Promise((resolve) => { setTimeout(resolve, 100); });
const containerElement = await waitForElement('.rnr-container');
expect(containerElement).to.exist;
// Verify that refreshToken was called or that fetch was called (meaning token was obtained)
const wasCalled = refreshTokenStub.called || fetchStub.called;
expect(wasCalled).to.be.true;
} finally {
window.adobeIMS = originalIMS;
}
}).timeout(5000);

it('should handle token expiring soon (within 5 minutes)', async () => {
const originalIMS = window.adobeIMS;
const refreshTokenStub = sinon.stub().resolves({
tokenInfo: {
token: 'new-refreshed-token',
expire: new Date(Date.now() + 10 * 60 * 1000),
},
});
// Mock token that will expire in 4 minutes
window.adobeIMS = {
getAccessToken: () => ({
token: 'expiring-soon-token',
expire: { valueOf: () => Date.now() + 4 * 60 * 1000 }, // 4 minutes from now
}),
refreshToken: refreshTokenStub,
};
const fetchStub = sinon.stub(window, 'fetch').resolves({
ok: true,
json: () => Promise.resolve({
overallRating: 4.5,
ratingHistogram: { rating1: 0, rating2: 0, rating3: 0, rating4: 5, rating5: 5 },
}),
});
try {
document.body.innerHTML = await readFile({ path: './mocks/body.html' });
const rnr = document.querySelector('.rnr');
await init(rnr);
// Wait a bit for async operations to complete
await new Promise((resolve) => { setTimeout(resolve, 100); });
const containerElement = await waitForElement('.rnr-container');
expect(containerElement).to.exist;
// Verify that refreshToken was called or that fetch was called (meaning token was obtained)
const wasCalled = refreshTokenStub.called || fetchStub.called;
expect(wasCalled).to.be.true;
} finally {
window.adobeIMS = originalIMS;
}
}).timeout(5000);

it('should gracefully handle fetch errors when loading data', async () => {
sinon.stub(window, 'fetch').rejects(new Error('Network error'));
Expand Down
7 changes: 6 additions & 1 deletion test/e2e/features/unity/smoke/verbs.feature
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,9 @@ Feature: Frictionless Converter Block
@ai-summary-generator
Examples:
| Verb | File |
| ai-summary-generator | test-files/test.pdf |
| ai-summary-generator | test-files/test.pdf |

@pdf-ai
Examples:
| Verb | File |
| pdf-ai | test-files/test.pdf |
7 changes: 7 additions & 0 deletions test/e2e/page-objects/pdfai.page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FrictionlessPage } from './frictionless.page';

export class PdfAiPage extends FrictionlessPage {
constructor() {
super("/acrobat/online/pdf-ai");
}
}
4 changes: 3 additions & 1 deletion test/e2e/step-definitions/dc.steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { AddPdfPageNumbersPage } from "../page-objects/addpdfpagenumbers.page";
import { OcrPdfPage } from "../page-objects/ocrpdf.page";
import { AiChatPdfPage } from "../page-objects/aichatpdf.page";
import { AiSummaryGeneratorPage } from "../page-objects/aisummarygenerator.page";
import { PdfAiPage } from "../page-objects/pdfai.page";
import { FrictionlessPage } from "../page-objects/frictionless.page";
import { UnityPage } from "../page-objects/unity.page";
import { DCPage } from "../page-objects/dc.page";
Expand Down Expand Up @@ -72,7 +73,8 @@ Then(/^I go to the ([^\"]*) page$/, async function (verb) {
"add-pdf-page-numbers": AddPdfPageNumbersPage,
"ocr-pdf": OcrPdfPage,
"ai-chat-pdf": AiChatPdfPage,
"ai-summary-generator": AiSummaryGeneratorPage
"ai-summary-generator": AiSummaryGeneratorPage,
"pdf-ai": PdfAiPage
}[verb];
this.page = new pageClass();

Expand Down
Loading