Skip to content
Open
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
106 changes: 100 additions & 6 deletions test/core/workflow/workflow-acrobat/action-binder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,6 @@ describe('ActionBinder', () => {
describe('isDirectUploadVerb', () => {
beforeEach(() => {
actionBinder.workflowCfg.enabledFeatures = ['word-to-pdf'];
actionBinder.workflowCfg.targetCfg.directUploadVerbs = ['word-to-pdf'];
actionBinder.workflowCfg.targetCfg.directUploadMaxSize = 1048576;
});

Expand All @@ -1538,11 +1537,6 @@ describe('ActionBinder', () => {
it('should return false for direct upload verbs without file size', () => {
expect(actionBinder.isDirectUploadVerb()).to.be.false;
});

it('should return false for verbs not configured for direct upload', () => {
actionBinder.workflowCfg.enabledFeatures = ['compress-pdf'];
expect(actionBinder.isDirectUploadVerb(500000)).to.be.false;
});
});

describe('continueInApp', () => {
Expand Down Expand Up @@ -3142,6 +3136,106 @@ describe('ActionBinder', () => {
});
});

describe('filterFilesWithPdflite - integrity/acroform/scanned checks', () => {
let originalPdflite;
let pdfDetailsStub;

const pdf = (name) => new File(['%PDF-1.4 test'], name, { type: 'application/pdf' });

beforeEach(() => {
originalPdflite = window.pdflite;
actionBinder.MULTI_FILE = false;
actionBinder.multiFileValidationFailure = false;
actionBinder.limits = { pageLimit: { maxNumPages: 100 } };
actionBinder.workflowCfg = {
enabledFeatures: ['stylize'],
targetCfg: {
pdfIntegrityCheckVerbs: ['stylize'],
pdfAcroformCheckVerbs: ['stylize'],
pdfScannedCheckVerbs: ['stylize'],
},
};
sinon.stub(actionBinder, 'dispatchErrorToast').resolves();
pdfDetailsStub = sinon.stub().returns({ NUM_PAGES: 1 });
window.pdflite = { pdfDetails: pdfDetailsStub };
});

afterEach(() => {
window.pdflite = originalPdflite;
});

it('excludes AcroForm files and dispatches the acroform error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 1, HAS_ACROFORM: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('form.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_acroform_not_supported')).to.be.true;
});

it('excludes scanned files and dispatches the scanned-document error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 1, IS_SCANNED_DOCUMENT: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('scanned.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_scanned_document')).to.be.true;
});

it('excludes empty files with the empty-file error, not the corrupt/encrypted one', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 0, IS_EMPTY: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('empty.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_empty_file')).to.be.true;
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_password_protected')).to.be.false;
});

it('excludes encrypted/password-protected files via the integrity error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 1, IS_ENCRYPTED: true });
const result = await actionBinder.filterFilesWithPdflite([pdf('encrypted.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_password_protected')).to.be.true;
});

it('treats a fileDetails failure as a corrupted file via the integrity error', async () => {
pdfDetailsStub.returns({ error: 'not found' });
const result = await actionBinder.filterFilesWithPdflite([pdf('corrupt.pdf')]);
expect(result).to.have.lengthOf(0);
expect(actionBinder.dispatchErrorToast.calledWith('validation_error_password_protected')).to.be.true;
});

it('passes checkScanned=true to pdflite when the verb is in pdfScannedCheckVerbs', async () => {
await actionBinder.filterFilesWithPdflite([pdf('a.pdf')]);
expect(pdfDetailsStub.calledWith(sinon.match.any, true)).to.be.true;
});

it('does not request the scan when the verb is not in pdfScannedCheckVerbs', async () => {
actionBinder.workflowCfg.targetCfg.pdfScannedCheckVerbs = [];
await actionBinder.filterFilesWithPdflite([pdf('a.pdf')]);
expect(pdfDetailsStub.calledWith(sinon.match.any, false)).to.be.true;
});

it('passes a clean PDF through without dispatching an error', async () => {
pdfDetailsStub.returns({ NUM_PAGES: 5, HAS_ACROFORM: false, IS_ENCRYPTED: false, IS_SCANNED_DOCUMENT: false });
const file = pdf('clean.pdf');
const result = await actionBinder.filterFilesWithPdflite([file]);
expect(result).to.deep.equal([file]);
expect(actionBinder.dispatchErrorToast.called).to.be.false;
});

it('skips pdflite entirely for verbs not in any check list and without page limits', async () => {
actionBinder.limits = {};
actionBinder.workflowCfg.targetCfg = {};
const files = [pdf('x.pdf')];
const result = await actionBinder.filterFilesWithPdflite(files);
expect(result).to.equal(files);
expect(pdfDetailsStub.called).to.be.false;
});

it('sets multiFileValidationFailure when a file is excluded in multi-file mode', async () => {
actionBinder.MULTI_FILE = true;
pdfDetailsStub.returns({ NUM_PAGES: 1, HAS_ACROFORM: true });
await actionBinder.filterFilesWithPdflite([pdf('form.pdf')]);
expect(actionBinder.multiFileValidationFailure).to.be.true;
});
});

describe('ensurePageConfig', () => {
let originalFetch;

Expand Down
44 changes: 44 additions & 0 deletions test/unitylibs/scripts/pdflite-validator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,50 @@ describe('PDFLite Validator', () => {
});
});

describe('encrypted / password-protected flags (black box)', () => {
it('returns a results entry for every input file', async () => {
const files = [
{ type: 'application/pdf', name: 'a.pdf' },
{ type: 'application/pdf', name: 'b.pdf' },
];
const limits = { pageLimit: { maxNumPages: 100 } };

const result = await validateFilesWithPdflite(files, limits);

expect(result).to.have.property('results');
expect(result.results).to.have.lengthOf(files.length);
});

it('passes files through gracefully when pdflite cannot read them (no throw)', async () => {
const files = [{ type: 'application/pdf', name: 'unreadable.pdf' }];
const limits = { pageLimit: { maxNumPages: 100 } };

const result = await validateFilesWithPdflite(files, limits);

expect(result.passed.length + result.failed.length).to.equal(files.length);
result.results.forEach((r) => {
if (r.ok === false) {
expect(['OVER_MAX_PAGE_COUNT', 'UNDER_MIN_PAGE_COUNT']).to.include(r.errorType);
}
});
});

it('never flags non-PDF files as encrypted or password-protected', async () => {
const files = [
{ type: 'image/jpeg', name: 'photo.jpg' },
{ type: 'application/pdf', name: 'doc.pdf' },
];
const limits = { pageLimit: { maxNumPages: 100 } };

const result = await validateFilesWithPdflite(files, limits);

const jpegResult = result.results.find((r) => r.file.name === 'photo.jpg');
expect(jpegResult.ok).to.equal(true);
expect(jpegResult.isEncrypted).to.be.undefined;
expect(jpegResult.isPasswordProtected).to.be.undefined;
});
});

describe('getPageCountErrorCode', () => {
const SINGLE_FILE_ERRORS = {
OVER_MAX_PAGE_COUNT: 'upload_validation_error_max_page_count',
Expand Down
5 changes: 2 additions & 3 deletions unitylibs/core/widgets/prompt-bar-audio/prompt-bar-audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,6 @@ class UnityWidget {
'aria-label': 'model type',
'aria-haspopup': 'listbox',
role: 'combobox',
'aria-labelledby': 'listbox-label',
'data-selected-model-id': selectedModelType,
'data-selected-model-version': selectedModelVersion,
'data-selected-model-module': selectedModelModule,
Expand All @@ -346,7 +345,7 @@ class UnityWidget {
this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType);
this.selectedModelText = models[0].name.trim();
const menuIcon = createTag('span', { class: 'menu-icon' }, '<svg><use xlink:href="#unity-chevron-icon"></use></svg>');
const listItems = createTag('ul', { class: 'verb-list', id: 'model-menu', role: 'listbox', 'aria-labelledby': 'listbox-label' });
const listItems = createTag('ul', { class: 'verb-list', id: 'model-menu', role: 'listbox', 'aria-label': 'Model options' });
listItems.setAttribute('style', 'display: none;');
selectedElement.append(menuIcon);
const handleDocumentClick = (e) => {
Expand Down Expand Up @@ -875,7 +874,7 @@ function createPromptAudioInputShell(widgetInstance, el, defaultPrompt, analytic
widgetInstance.hasModelOptions = !!el.querySelector('[class*="icon-model"]');
widgetInstance.verbDropdown();
const modelParts = widgetInstance.modelDropdown();
const promptLabelText = placeholderRowText(el, 'placeholder-prompt-label');
const promptLabelText = placeholderRowText(el, 'placeholder-prompt-label') || 'Enter prompt';
const inpField = createPromptAudioInputField(widgetInstance, defaultPrompt, pws);
const actionContainer = createPromptAudioActionContainer(widgetInstance, widgetWrap, modelParts);
const genBtn = createPromptAudioGenerateButton(widgetInstance, el, pws);
Expand Down
50 changes: 40 additions & 10 deletions unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.css
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,26 @@
scrollbar-color: #888 transparent;
}

.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .inp-fieldset {
display: flex;
flex-flow: inherit;
align-items: inherit;
justify-content: inherit;
gap: inherit;
width: 100%;
border: none;
margin: 0;
padding: 0;
min-width: 0;
text-align: start;
}

.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .unity-slf-copy-label {
font-size: var(--type-body-xs-size);
font-weight: 400;
margin-inline-start: 2px;
padding: 0;
border: none;
}

.unity-enabled .interactive-area .ex-unity-wrap:not(.verb-options) .ex-unity-widget .inp-wrap .unity-slf-copy-label {
Expand Down Expand Up @@ -954,6 +970,7 @@
background: transparent;
resize: none;
margin: 0;
color: #F8F8F8;
scrollbar-width: thin;
scrollbar-color: #888 transparent;
}
Expand Down Expand Up @@ -1250,18 +1267,31 @@
align-items: center;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .unity-slf-copy-label,
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .inp-fieldset {
display: grid;
grid-template-columns: 1fr auto;
column-gap: 8px;
align-items: center;
grid-column: 1 / -1;
width: 100%;
border: none;
margin: 0;
padding: 0;
min-width: 0;
text-align: start;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .unity-slf-copy-label,
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > label {
grid-column: 1 / -1;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .inp-field {
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .inp-field {
grid-column: 1 / -1;
color: #F8F8F8;
padding: 10px 0px;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container {
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .action-container {
grid-column: 1;
margin-top: 24px;
display: flex;
Expand All @@ -1271,7 +1301,7 @@
min-width: 0;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .act-wrap {
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .act-wrap {
grid-column: 2;
margin-top: 24px;
display: flex;
Expand All @@ -1280,23 +1310,23 @@
align-self: center;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container:empty {
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .action-container:empty {
display: none;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container:empty + .act-wrap {
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .action-container:empty + .act-wrap {
grid-column: 1 / -1;
justify-self: end;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .models-container,
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .verbs-container {
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .action-container > .models-container,
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .action-container > .verbs-container {
display: flex;
justify-content: flex-start;
align-items: center;
}

.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap > .action-container > .models-container {
.unity-prompt-bar-style.unity-enabled .interactive-area .ex-unity-wrap .ex-unity-widget .inp-wrap .action-container > .models-container {
min-width: 0;
max-width: 173px;
}
Expand Down
17 changes: 10 additions & 7 deletions unitylibs/core/widgets/prompt-bar-style/prompt-bar-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export class UnityWidget {
'aria-label': 'media type',
'aria-haspopup': 'listbox',
role: 'combobox',
'aria-labelledby': 'listbox-label',
'data-selected-verb': selectedVerbType,
}, `${selectedVerb?.textContent.trim()}`);
this.selectedVerbType = selectedVerbType;
Expand All @@ -63,7 +62,7 @@ export class UnityWidget {
}
this.widgetWrap.classList.add('verb-options');
const menuIcon = createTag('span', { class: 'menu-icon' }, '<svg><use xlink:href="#unity-chevron-icon"></use></svg>');
const verbList = createTag('ul', { class: 'verb-list', id: 'media-menu', role: 'listbox', 'aria-labelledby': 'listbox-label' });
const verbList = createTag('ul', { class: 'verb-list', id: 'media-menu', role: 'listbox', 'aria-label': 'Media options' });
verbList.setAttribute('style', 'display: none;');
selectedElement.append(menuIcon);
const handleDocumentClick = (e) => {
Expand Down Expand Up @@ -345,7 +344,6 @@ export class UnityWidget {
'aria-label': 'model type',
'aria-haspopup': 'listbox',
role: 'combobox',
'aria-labelledby': 'listbox-label',
'data-selected-model-id': selectedModelType,
'data-selected-model-version': selectedModelVersion,
'data-selected-model-module': selectedModelModule,
Expand All @@ -360,7 +358,7 @@ export class UnityWidget {
this.widgetWrap.setAttribute('data-selected-verb', this.selectedVerbType);
this.selectedModelText = models[0].name.trim();
const menuIcon = createTag('span', { class: 'menu-icon' }, '<svg><use xlink:href="#unity-chevron-icon"></use></svg>');
const listItems = createTag('ul', { class: 'verb-list', id: 'model-menu', role: 'listbox', 'aria-labelledby': 'listbox-label' });
const listItems = createTag('ul', { class: 'verb-list', id: 'model-menu', role: 'listbox', 'aria-label': 'Model options' });
listItems.setAttribute('style', 'display: none;');
selectedElement.append(menuIcon);
const handleDocumentClick = (e) => {
Expand Down Expand Up @@ -595,8 +593,12 @@ async function createPromptInputShell(widgetInstance, el, styles) {
const modelParts = widgetInstance.modelDropdown();
const promptLabelText = placeholderRowText(el, 'icon-placeholder-prompt');
const inpWrap = createTag('div', { class: 'inp-wrap' });
const labelText = promptLabelText || 'Prompt';
const promptLabel = createTag('label', { for: 'promptInput', class: 'unity-slf-copy-label unity-slf-prompt-label' }, labelText);
const labelText = promptLabelText || 'Enter prompt';
const hasDropdowns = verbParts.length > 1 || modelParts.length > 1;
const inpGroup = hasDropdowns ? createTag('fieldset', { class: 'inp-fieldset' }) : inpWrap;
const promptLabel = hasDropdowns
? createTag('legend', { class: 'unity-slf-copy-label unity-slf-prompt-label' }, labelText)
: createTag('label', { for: 'promptInput', class: 'unity-slf-copy-label unity-slf-prompt-label' }, labelText);
const inpField = createTag('textarea', {
id: 'promptInput',
class: 'inp-field',
Expand Down Expand Up @@ -659,7 +661,8 @@ async function createPromptInputShell(widgetInstance, el, styles) {
}
}
actWrap.append(genBtn);
inpWrap.append(promptLabel, inpField, actionContainer, actWrap);
inpGroup.append(promptLabel, inpField, actionContainer, actWrap);
if (hasDropdowns) inpWrap.append(inpGroup);
const comboboxContainer = createTag('div', { class: 'autocomplete' });
comboboxContainer.append(inpWrap);
widget.append(comboboxContainer);
Expand Down
Loading
Loading