=> {
diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.html b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.html
index 93a7d13688..72a0d0b526 100644
--- a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.html
+++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.html
@@ -202,6 +202,7 @@
[dirty]="ngControl?.dirty"
[errors]="ngControl?.errors"
>
+
@for (rejectedFile of rejectedFiles; track rejectedFile) {
@if (rejectedFile.errorType === 'fileType') {
diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.html b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.html
index 666dcf4c48..e4b11420ac 100644
--- a/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.html
+++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.html
@@ -7,6 +7,6 @@
/>
-{{fileDrop.value | json}} @for (file of fileDrop.value; track file) {
+@for (file of fileDrop.value; track file) {
}
diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.ts
index eef8088cef..7a0ccf5327 100644
--- a/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.ts
+++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/fixtures/reactive-file-drop.component.fixture.ts
@@ -1,4 +1,3 @@
-import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import {
FormBuilder,
@@ -14,7 +13,7 @@ import { SkyFileDropModule } from '../file-drop.module';
import { SkyFileLink } from '../file-link';
@Component({
- imports: [SkyFileDropModule, FormsModule, ReactiveFormsModule, CommonModule],
+ imports: [SkyFileDropModule, FormsModule, ReactiveFormsModule],
selector: 'sky-file-drop-reactive-test',
standalone: true,
templateUrl: './reactive-file-drop.component.fixture.html',
diff --git a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.spec.ts b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.spec.ts
index 87913dbbe4..e6f030fc31 100644
--- a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.spec.ts
+++ b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.spec.ts
@@ -1,24 +1,95 @@
+import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
-import { Component } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { SkyFileDropChange, SkyFileDropModule } from '@skyux/forms';
-
-import { ReplaySubject, firstValueFrom } from 'rxjs';
+import {
+ FormBuilder,
+ FormControl,
+ FormGroup,
+ FormsModule,
+ ReactiveFormsModule,
+} from '@angular/forms';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { provideSkyFileReaderTesting } from '@skyux/core/testing';
+import {
+ SkyFileDropModule,
+ SkyFileItem,
+ SkyFileLink,
+ SkyFileValidateFn,
+} from '@skyux/forms';
import { SkyFileDropHarness } from './file-drop-harness';
+import { SkyFileItemHarness } from './file-item-harness';
@Component({
- imports: [SkyFileDropModule],
- template: ``,
+ standalone: true,
+ imports: [SkyFileDropModule, FormsModule, ReactiveFormsModule],
+ template: `
+
+ @for (file of fileDrop.value; track file) {
+
+ }
+ `,
})
class TestComponent {
- public filesChanged = new ReplaySubject(1);
+ public acceptedTypes: string | undefined;
+ public allItems: (SkyFileItem | SkyFileLink)[] = [];
+ public allowLinks = true;
+ public fileUploadAriaLabel: string | undefined;
+ public helpPopoverContent: string | undefined;
+ public helpPopoverTitle: string | undefined;
+ public hintText: string | undefined;
+ public labelHidden = false;
+ public labelText: string | undefined = 'File upload';
+ public linkUploadAriaLabel: string | undefined;
+ public linkUploadHintText: string | undefined;
+ public maxFileSize: number | undefined;
+ public minFileSize: number | undefined;
+ public required = false;
+ public showCustomError = false;
+ public stacked = false;
+ public validateFunction: SkyFileValidateFn | undefined;
+
+ public fileDrop: FormControl = new FormControl(undefined);
+ public formGroup: FormGroup = inject(FormBuilder).group({
+ fileDrop: this.fileDrop,
+ });
- public onFilesChanged(event: SkyFileDropChange): void {
- this.filesChanged.next(event);
+ public deleteFile(file: SkyFileItem | SkyFileLink): void {
+ const index = this.fileDrop.value.indexOf(file);
+ if (index !== -1) {
+ this.fileDrop.value?.splice(index, 1);
+ }
+ if (this.fileDrop.value.length === 0) {
+ this.fileDrop.setValue(null);
+ }
}
}
@@ -26,7 +97,13 @@ describe('File drop harness', () => {
async function setupTest(): Promise<{
harness: SkyFileDropHarness;
fixture: ComponentFixture;
+ loader: HarnessLoader;
}> {
+ await TestBed.configureTestingModule({
+ imports: [TestComponent, NoopAnimationsModule],
+ providers: [provideSkyFileReaderTesting, provideSkyFileReaderTesting()],
+ }).compileComponents();
+
const fixture = TestBed.createComponent(TestComponent);
const loader = TestbedHarnessEnvironment.loader(fixture);
const harness: SkyFileDropHarness = await loader.getHarness(
@@ -35,28 +112,338 @@ describe('File drop harness', () => {
}),
);
- return { harness, fixture };
+ return { harness, fixture, loader };
}
- it('should drop files', async () => {
+ it('should drop a single file', async () => {
const { fixture, harness } = await setupTest();
const testFile = new File([], 'test.png');
- const changedFiles = firstValueFrom(fixture.componentInstance.filesChanged);
+ await harness.loadFile(testFile);
+
+ expect(fixture.componentInstance.fileDrop.value).toEqual([
+ {
+ file: testFile,
+ url: jasmine.any(String),
+ },
+ ]);
+ });
+
+ it('should get accepted types', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.acceptedTypes = 'image/png';
+ fixture.detectChanges();
- await harness.dropFile(testFile);
+ await expectAsync(harness.getAcceptedTypes()).toBeResolvedTo('image/png');
+ });
+
+ it('should get file upload button aria-label', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.fileUploadAriaLabel =
+ 'this is an accessibility label.';
+ fixture.detectChanges();
+
+ await expectAsync(harness.getFileUploadAriaLabel()).toBeResolvedTo(
+ 'this is an accessibility label.',
+ );
+ });
+
+ it('should click the file upload button', async () => {
+ const { fixture, harness } = await setupTest();
+
+ const input = fixture.nativeElement.querySelector(
+ 'input.sky-file-input-hidden',
+ );
+ spyOn(input, 'click');
+
+ await harness.clickFileDropTarget();
+ expect(input.click).toHaveBeenCalled();
+ });
+
+ it('should throw an error if there is no help inline', async () => {
+ const { harness } = await setupTest();
+
+ await expectAsync(harness.getHelpPopoverContent()).toBeRejectedWithError(
+ 'No help inline found.',
+ );
+ });
+
+ it('should get help popover content', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.helpPopoverContent = 'Upload a file';
+ fixture.detectChanges();
+
+ await harness.clickHelpInline();
+
+ await expectAsync(harness.getHelpPopoverContent()).toBeResolvedTo(
+ 'Upload a file',
+ );
+ });
+
+ it('should get help popover title', async () => {
+ const { fixture, harness } = await setupTest();
- const files = await changedFiles;
+ fixture.componentInstance.helpPopoverContent = 'Upload a file';
+ fixture.componentInstance.helpPopoverTitle = 'Help';
+ fixture.detectChanges();
- expect(files).toEqual({
- files: [
+ await harness.clickHelpInline();
+
+ await expectAsync(harness.getHelpPopoverTitle()).toBeResolvedTo('Help');
+ });
+
+ it('should get hint text', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.hintText = 'this is a hint.';
+ fixture.detectChanges();
+
+ await expectAsync(harness.getHintText()).toBeResolvedTo('this is a hint.');
+ });
+
+ it('should get label text', async () => {
+ const { harness } = await setupTest();
+
+ await expectAsync(harness.getLabelText()).toBeResolvedTo('File upload');
+ });
+
+ it('should get whether the label is hidden', async () => {
+ const { fixture, harness } = await setupTest();
+
+ await expectAsync(harness.isLabelHidden()).toBeResolvedTo(false);
+
+ fixture.componentInstance.labelHidden = true;
+ fixture.detectChanges();
+
+ await expectAsync(harness.isLabelHidden()).toBeResolvedTo(true);
+ });
+
+ it('should get whether file drop is required', async () => {
+ const { fixture, harness } = await setupTest();
+
+ await expectAsync(harness.isRequired()).toBeResolvedTo(false);
+
+ fixture.componentInstance.required = true;
+ fixture.detectChanges();
+
+ await expectAsync(harness.isRequired()).toBeResolvedTo(true);
+ });
+
+ it('should get whether file drop is stacked', async () => {
+ const { fixture, harness } = await setupTest();
+
+ await expectAsync(harness.isStacked()).toBeResolvedTo(false);
+
+ fixture.componentInstance.stacked = true;
+ fixture.detectChanges();
+
+ await expectAsync(harness.isStacked()).toBeResolvedTo(true);
+ });
+
+ it('should get whether required error has fired', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.required = true;
+ fixture.detectChanges();
+
+ fixture.componentInstance.fileDrop.markAsTouched();
+ fixture.detectChanges();
+
+ await expectAsync(harness.hasRequiredError()).toBeResolvedTo(true);
+ });
+
+ it('should get whether file type error has fired', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.acceptedTypes = 'image/png';
+ fixture.detectChanges();
+
+ await harness.loadFiles([
+ new File([], 'wrongFile.jpg', { type: 'image/jpg' }),
+ ]);
+
+ await expectAsync(harness.hasFileTypeError()).toBeResolvedTo(true);
+ });
+
+ it('should get whether max file type error has fired', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.maxFileSize = -1;
+ fixture.detectChanges();
+
+ await harness.loadFiles([
+ new File([], 'wrongFile.jpg', { type: 'image/jpg' }),
+ ]);
+
+ await expectAsync(harness.hasMaxFileSizeError()).toBeResolvedTo(true);
+ });
+
+ it('should get whether min file type error has fired', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.minFileSize = 1000;
+ fixture.detectChanges();
+
+ await harness.loadFiles([
+ new File([], 'wrongFile.jpg', { type: 'image/jpg' }),
+ ]);
+
+ await expectAsync(harness.hasMinFileSizeError()).toBeResolvedTo(true);
+ });
+
+ it('should get whether validate file type error has fired', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.validateFunction = function (
+ file: SkyFileItem,
+ ): string | undefined {
+ return file.file.name.startsWith('a')
+ ? 'Upload a file that does not begin with the letter "a"'
+ : undefined;
+ };
+ fixture.detectChanges();
+
+ await harness.loadFiles([
+ new File([], 'aWrongFile.jpg', { type: 'image/jpg' }),
+ ]);
+
+ await expectAsync(harness.hasValidateFnError()).toBeResolvedTo(true);
+ });
+
+ it('should get whether custom error has fired', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.fileDrop.markAsTouched();
+ fixture.detectChanges();
+ fixture.componentInstance.showCustomError = true;
+ fixture.detectChanges();
+
+ await expectAsync(harness.hasCustomError('customError')).toBeResolvedTo(
+ true,
+ );
+ });
+
+ describe('upload link harness', () => {
+ it('should get upload link aria-label', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.linkUploadAriaLabel = 'upload aria-label';
+ fixture.detectChanges();
+
+ await expectAsync(harness.getLinkUploadAriaLabel()).toBeResolvedTo(
+ 'upload aria-label',
+ );
+ });
+
+ it('should get the upload link hint text', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.linkUploadHintText = 'link hint text';
+ fixture.detectChanges();
+
+ await expectAsync(harness.getLinkUploadHintText()).toBeResolvedTo(
+ 'link hint text',
+ );
+ });
+
+ it('should throw an error clicking a disabled upload link button', async () => {
+ const { harness } = await setupTest();
+
+ await expectAsync(
+ harness.clickLinkUploadDoneButton(),
+ ).toBeRejectedWithError(
+ 'Done button is disabled and cannot be clicked. Enter text to enable link upload.',
+ );
+ });
+
+ it('should throw an error if attempting to interact with link upload when links are not allowed', async () => {
+ const { fixture, harness } = await setupTest();
+
+ fixture.componentInstance.allowLinks = false;
+ fixture.detectChanges();
+
+ await expectAsync(harness.getLinkUploadAriaLabel()).toBeRejectedWithError(
+ 'Link upload cannot be found. Set `allowLinks` property to `true`.',
+ );
+ });
+
+ it('should upload a link', async () => {
+ const { fixture, harness } = await setupTest();
+
+ await harness.enterLinkUploadText('foo.bar');
+ await harness.clickLinkUploadDoneButton();
+
+ expect(fixture.componentInstance.fileDrop.value).toEqual([
{
- file: testFile,
- url: jasmine.any(String),
+ url: 'foo.bar',
},
- ],
- rejectedFiles: [],
+ ]);
+ });
+ });
+
+ describe('sky file item harness', () => {
+ async function getFileItemHarness(
+ harness: SkyFileDropHarness,
+ loader: HarnessLoader,
+ ): Promise {
+ await harness.loadFiles([
+ new File(['a'.repeat(20)], 'FileName', { type: 'image/png' }),
+ ]);
+ return await loader.getHarness(SkyFileItemHarness);
+ }
+ it('should get the file name', async () => {
+ const { harness, loader } = await setupTest();
+
+ const fileItemHarness = await getFileItemHarness(harness, loader);
+
+ await expectAsync(fileItemHarness.getFileName()).toBeResolvedTo(
+ 'FileName',
+ );
+ });
+
+ it('should get the file size', async () => {
+ const { harness, loader } = await setupTest();
+
+ const fileItemHarness = await getFileItemHarness(harness, loader);
+
+ await expectAsync(fileItemHarness.getFileSize()).toBeResolvedTo(
+ '20 bytes',
+ );
+ });
+
+ it('should click the delete button', async () => {
+ const { fixture, harness, loader } = await setupTest();
+
+ const fileItemHarness = await getFileItemHarness(harness, loader);
+
+ expect(fixture.componentInstance.fileDrop.value.length).toBe(1);
+
+ await fileItemHarness.clickDeleteButton();
+
+ expect(fixture.componentInstance.fileDrop.value).toBe(null);
+ });
+
+ it('should get sky file item by file name', async () => {
+ const { harness, loader } = await setupTest();
+
+ await harness.loadFiles([
+ new File(['a'.repeat(20)], 'FileName', { type: 'image/png' }),
+ new File(['a'.repeat(10)], 'OtherFile', { type: 'image/png' }),
+ ]);
+
+ const fileItemHarness = await loader.getHarness(
+ SkyFileItemHarness.with({
+ fileName: 'OtherFile',
+ }),
+ );
+
+ await expectAsync(fileItemHarness.getFileSize()).toBeResolvedTo(
+ '10 bytes',
+ );
});
});
});
diff --git a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.ts b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.ts
index 7cc20e8a22..22065c49d7 100644
--- a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.ts
+++ b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-harness.ts
@@ -1,11 +1,14 @@
import { EventData, HarnessPredicate } from '@angular/cdk/testing';
import { SkyComponentHarness } from '@skyux/core/testing';
+import { SkyHelpInlineHarness } from '@skyux/help-inline/testing';
+
+import { SkyFormErrorsHarness } from '../../form-error/form-errors-harness';
import { SkyFileDropHarnessFilters } from './file-drop-harness-filters';
+import { SkyFileDropLinkUploadHarness } from './file-drop-link-upload-harness';
/**
* Harness for interacting with a file drop component in tests.
- * @internal
*/
export class SkyFileDropHarness extends SkyComponentHarness {
/**
@@ -14,6 +17,10 @@ export class SkyFileDropHarness extends SkyComponentHarness {
public static hostSelector = 'sky-file-drop';
#getDropTarget = this.locatorFor('.sky-file-drop-target');
+ #input = this.locatorFor('input[type="file"]');
+ #fileUploadButton = this.locatorFor('button.sky-file-drop-target');
+ #label = this.locatorFor('.sky-file-drop-label-text');
+ #formErrors = this.locatorForAll(SkyFormErrorsHarness);
/**
* Gets a `HarnessPredicate` that can be used to search for a
@@ -26,25 +33,213 @@ export class SkyFileDropHarness extends SkyComponentHarness {
}
/**
- * Drops a file onto the component's drop target.
+ * Clicks the file drop target.
+ */
+ public async clickFileDropTarget(): Promise {
+ return await (await this.#fileUploadButton()).click();
+ }
+
+ /**
+ * Clicks the help inline button.
+ */
+ public async clickHelpInline(): Promise {
+ return await (await this.#getHelpInline()).click();
+ }
+
+ /**
+ * Clicks the link upload `Done` button'.
+ */
+ public async clickLinkUploadDoneButton(): Promise {
+ return await (await this.#getLinkUpload()).clickDoneButton();
+ }
+
+ /**
+ * Enters text into the link upload input.
+ */
+ public async enterLinkUploadText(link: string): Promise {
+ return await (await this.#getLinkUpload()).enterText(link);
+ }
+
+ /**
+ * Gets the accepted file types.
+ */
+ public async getAcceptedTypes(): Promise {
+ return await (await this.#input()).getAttribute('accept');
+ }
+
+ /**
+ * Gets the aria-label for the file upload button.
+ */
+ public async getFileUploadAriaLabel(): Promise {
+ return await (await this.#fileUploadButton()).getAttribute('aria-label');
+ }
+
+ /**
+ * Gets the help inline popover content.
+ */
+ public async getHelpPopoverContent(): Promise {
+ const content = await (await this.#getHelpInline()).getPopoverContent();
+
+ return content as string | undefined;
+ }
+
+ /**
+ * Gets the help inline popover title.
+ */
+ public async getHelpPopoverTitle(): Promise {
+ return await (await this.#getHelpInline()).getPopoverTitle();
+ }
+
+ /**
+ * Gets the hint text.
+ */
+ public async getHintText(): Promise {
+ return (
+ await (await this.locatorFor('.sky-file-drop-hint-text')()).text()
+ ).trim();
+ }
+
+ /**
+ * Gets the label text.
+ */
+ public async getLabelText(): Promise {
+ return (await (await this.#label()).text()).trim();
+ }
+
+ /**
+ * Gets the link upload aria-label.
+ */
+ public async getLinkUploadAriaLabel(): Promise {
+ return await (await this.#getLinkUpload()).getAriaLabel();
+ }
+
+ /**
+ * Gets the link upload hint text.
*/
- public async dropFile(file: File): Promise {
+ public async getLinkUploadHintText(): Promise {
+ return await (await this.#getLinkUpload()).getHintText();
+ }
+
+ /**
+ * Whether a custom form error has fired.
+ */
+ public async hasCustomError(errorName: string): Promise {
+ return await (await this.#getFormErrors()).hasError(errorName);
+ }
+
+ /**
+ * Whether the file type error has fired.
+ */
+ public async hasFileTypeError(): Promise {
+ return await (await this.#getFormErrors()).hasError('fileType');
+ }
+
+ /**
+ * Whether the max file size error has fired.
+ */
+ public async hasMaxFileSizeError(): Promise {
+ return await (await this.#getFormErrors()).hasError('maxFileSize');
+ }
+
+ /**
+ * Whether the min file size error has fired.
+ */
+ public async hasMinFileSizeError(): Promise {
+ return await (await this.#getFormErrors()).hasError('minFileSize');
+ }
+
+ /**
+ * Whether the required error has fired.
+ */
+ public async hasRequiredError(): Promise {
+ return await (await this.#getFormErrors()).hasError('required');
+ }
+
+ /**
+ * Whether the validate error from the customer validation has fired.
+ */
+ public async hasValidateFnError(): Promise {
+ return await (await this.#getFormErrors()).hasError('validate');
+ }
+
+ /**
+ * Whether label text is hidden.
+ */
+ public async isLabelHidden(): Promise {
+ return await (
+ await this.locatorFor('legend.sky-control-label')()
+ ).hasClass('sky-screen-reader-only');
+ }
+
+ /**
+ * Whether file drop is required.
+ */
+ public async isRequired(): Promise {
+ return await (await this.#label()).hasClass('sky-control-label-required');
+ }
+
+ /**
+ * Whether file drop has stacked enabled.
+ */
+ public async isStacked(): Promise {
+ return await (await this.host()).hasClass('sky-form-field-stacked');
+ }
+
+ /**
+ * Loads a single file.
+ */
+ public async loadFile(file: File): Promise {
await this.#dropFiles([file]);
}
- // Consider making this public when we finalize this harness's public API.
- async #dropFiles(files: File[]): Promise {
+ /**
+ * Loads multiple files.
+ */
+ public async loadFiles(files: File[] | null): Promise {
+ return await this.#dropFiles(files);
+ }
+
+ async #dropFiles(files: File[] | null): Promise {
const dropTarget = await this.#getDropTarget();
- const fileList = {
- item: (index: number): File => files[index],
- length: files.length,
- };
+ type TestDataTransfer = DataTransfer & { [key: string]: EventData };
+
+ const dataTransfer = new DataTransfer() as TestDataTransfer;
+
+ files?.forEach((file) => {
+ dataTransfer.items.add(file);
+ });
await dropTarget.dispatchEvent('drop', {
- dataTransfer: {
- files: fileList as unknown as EventData,
- },
+ dataTransfer,
});
}
+
+ async #getFormErrors(): Promise {
+ return (await this.#formErrors())[1];
+ }
+
+ async #getHelpInline(): Promise {
+ const harness = await this.locatorForOptional(SkyHelpInlineHarness)();
+
+ if (harness) {
+ return harness;
+ }
+
+ throw Error('No help inline found.');
+ }
+
+ async #getLinkUpload(): Promise {
+ const linkUpload = await this.locatorForOptional(
+ SkyFileDropLinkUploadHarness,
+ )();
+
+ if (linkUpload) {
+ return linkUpload;
+ }
+
+ throw new Error(
+ 'Link upload cannot be found. Set `allowLinks` property to `true`.',
+ );
+ }
}
diff --git a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-link-upload-harness.ts b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-link-upload-harness.ts
new file mode 100644
index 0000000000..0c58c9ac56
--- /dev/null
+++ b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-link-upload-harness.ts
@@ -0,0 +1,57 @@
+import { SkyComponentHarness } from '@skyux/core/testing';
+
+import { SkyInputBoxHarness } from '../../input-box/input-box-harness';
+
+import { SkyFileDropLinkUploadInputHarness } from './file-drop-link-upload-input-harness';
+
+/**
+ * Harness for interacting with file drop component's link upload feature in tests.
+ * @internal
+ */
+export class SkyFileDropLinkUploadHarness extends SkyComponentHarness {
+ /**
+ * @internal
+ */
+ public static hostSelector = '.sky-file-drop-link';
+
+ #input = this.locatorFor(SkyFileDropLinkUploadInputHarness);
+ #inputBoxHarness = this.locatorFor(SkyInputBoxHarness);
+ #button = this.locatorFor('button.sky-btn-primary');
+
+ /**
+ * Clicks the `Done` button
+ */
+ public async clickDoneButton(): Promise {
+ if (await this.#isDoneDisabled()) {
+ throw new Error(
+ 'Done button is disabled and cannot be clicked. Enter text to enable link upload.',
+ );
+ }
+ return await (await this.#button()).click();
+ }
+
+ /**
+ * Enters text into the link upload input.
+ */
+ public async enterText(link: string): Promise {
+ return await (await this.#input()).setValue(link);
+ }
+
+ /**
+ * Gets the link upload aria-label.
+ */
+ public async getAriaLabel(): Promise {
+ return await (await this.#input()).getAriaLabel();
+ }
+
+ /**
+ * Gets the hint text.
+ */
+ public async getHintText(): Promise {
+ return await (await this.#inputBoxHarness()).getHintText();
+ }
+
+ async #isDoneDisabled(): Promise {
+ return (await (await this.#button()).getAttribute('disabled')) !== null;
+ }
+}
diff --git a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-link-upload-input-harness.ts b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-link-upload-input-harness.ts
new file mode 100644
index 0000000000..1c46234066
--- /dev/null
+++ b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-drop-link-upload-input-harness.ts
@@ -0,0 +1,18 @@
+import { SkyInputHarness } from '@skyux/core/testing';
+
+/**
+ * Harness to interact with the file drop link upload input harness.
+ */
+export class SkyFileDropLinkUploadInputHarness extends SkyInputHarness {
+ /**
+ * @internal
+ */
+ public static hostSelector = 'input.sky-form-control';
+
+ /**
+ * Gets the input aria-label
+ */
+ public async getAriaLabel(): Promise {
+ return await (await this.host()).getAttribute('aria-label');
+ }
+}
diff --git a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-item-harness-filters.ts b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-item-harness-filters.ts
new file mode 100644
index 0000000000..0a32805352
--- /dev/null
+++ b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-item-harness-filters.ts
@@ -0,0 +1,12 @@
+import { SkyHarnessFilters } from '@skyux/core/testing';
+
+/**
+ * A set of criteria that can be used to filter a list of `SkyFileItemHarness` instances.
+ */
+// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
+export interface SkyFileItemHarnessFilters extends SkyHarnessFilters {
+ /**
+ * Finds files whose file name matches this value.
+ */
+ fileName: string;
+}
diff --git a/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-item-harness.ts b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-item-harness.ts
new file mode 100644
index 0000000000..e4f4739b84
--- /dev/null
+++ b/libs/components/forms/testing/src/modules/file-attachment/file-drop/file-item-harness.ts
@@ -0,0 +1,52 @@
+import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
+
+import { SkyFileItemHarnessFilters } from './file-item-harness-filters';
+
+/**
+ * Harness for interacting with a file item component in tests.
+ */
+export class SkyFileItemHarness extends ComponentHarness {
+ /**
+ * @internal
+ */
+ public static hostSelector = 'sky-file-item';
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a
+ * `SkyFileItemHarness` that meets certain criteria.
+ */
+ public static with(
+ filters: SkyFileItemHarnessFilters,
+ ): HarnessPredicate {
+ return new HarnessPredicate(SkyFileItemHarness, filters).addOption(
+ 'fileName',
+ filters.fileName,
+ async (harness, fileName) => {
+ const harnessFileName = await harness.getFileName();
+ return await HarnessPredicate.stringMatches(harnessFileName, fileName);
+ },
+ );
+ }
+
+ /**
+ * Clicks the delete button.
+ */
+ public async clickDeleteButton(): Promise {
+ return await (await this.locatorFor('.sky-file-item-btn-delete')()).click();
+ }
+
+ /**
+ * Gets the file name.
+ */
+ public async getFileName(): Promise {
+ return await (await this.locatorFor('.sky-file-item-name')()).text();
+ }
+
+ /**
+ * Gets the file size.
+ */
+ public async getFileSize(): Promise {
+ const size = await (await this.locatorFor('.sky-file-item-size')()).text();
+ return size.substring(1, size.length - 1);
+ }
+}
diff --git a/libs/components/forms/testing/src/public-api.ts b/libs/components/forms/testing/src/public-api.ts
index 2a2e276df7..f4d55523bc 100644
--- a/libs/components/forms/testing/src/public-api.ts
+++ b/libs/components/forms/testing/src/public-api.ts
@@ -17,6 +17,8 @@ export { SkyFileAttachmentHarnessFilters } from './modules/file-attachment/file-
export { SkyFileDropHarness } from './modules/file-attachment/file-drop/file-drop-harness';
export { SkyFileDropHarnessFilters } from './modules/file-attachment/file-drop/file-drop-harness-filters';
export { provideSkyFileAttachmentTesting } from './modules/file-attachment/shared/provide-file-attachment-testing';
+export { SkyFileItemHarness } from './modules/file-attachment/file-drop/file-item-harness';
+export { SkyFileItemHarnessFilters } from './modules/file-attachment/file-drop/file-item-harness-filters';
export { SkyFormErrorsHarness } from './modules/form-error/form-errors-harness';
export { SkyFormErrorsHarnessFilters } from './modules/form-error/form-errors-harness.filters';