fix error sharing image and failing to upload, fix upload in webkit/safari, and test it
This commit is contained in:
@@ -73,7 +73,8 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
|
|
||||||
/* Configure global timeout */
|
/* Configure global timeout */
|
||||||
//pageTimeout: 30000,
|
// the image upload will often not succeed at 5 seconds
|
||||||
|
//timeout: 7000,
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export default class PhotoDialog extends Vue {
|
|||||||
const token = await accessToken(this.activeDid);
|
const token = await accessToken(this.activeDid);
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: "Bearer " + token,
|
Authorization: "Bearer " + token,
|
||||||
|
// axios fills in Content-Type of multipart/form-data
|
||||||
};
|
};
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
if (!this.blob) {
|
if (!this.blob) {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
export type Temp = {
|
export type Temp = {
|
||||||
id: string;
|
id: string;
|
||||||
blob?: Blob;
|
blob?: Blob; // deprecated because webkit (Safari) does not support Blob
|
||||||
|
blobB64?: string; // base64-encoded blob
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { createPeerDid } from "@/libs/crypto/vc/didPeer";
|
|||||||
|
|
||||||
export const PRIVACY_MESSAGE =
|
export const PRIVACY_MESSAGE =
|
||||||
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
|
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
|
||||||
|
export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64";
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
export const UNIT_SHORT: Record<string, string> = {
|
export const UNIT_SHORT: Record<string, string> = {
|
||||||
@@ -116,6 +117,38 @@ export const isGiveRecordTheUserCanConfirm = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function blobToBase64(blob: Blob): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => resolve(reader.result as string); // potential problem if it returns an ArrayBuffer?
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
||||||
|
// Extract the content type and the Base64 data
|
||||||
|
const [metadata, base64] = base64DataUrl.split(",");
|
||||||
|
const contentTypeMatch = metadata.match(/data:(.*?);base64/);
|
||||||
|
const contentType = contentTypeMatch ? contentTypeMatch[1] : "";
|
||||||
|
|
||||||
|
const byteCharacters = atob(base64);
|
||||||
|
const byteArrays = [];
|
||||||
|
|
||||||
|
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||||
|
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||||
|
|
||||||
|
const byteNumbers = new Array(slice.length);
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
byteNumbers[i] = slice.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
byteArrays.push(byteArray);
|
||||||
|
}
|
||||||
|
return new Blob(byteArrays, { type: contentType });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns the DID of the person who offered, or undefined if hidden
|
* @returns the DID of the person who offered, or undefined if hidden
|
||||||
* @param veriClaim is expected to have fields: claim and issuer
|
* @param veriClaim is expected to have fields: claim and issuer
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4" data-testid="imagery">
|
||||||
<span v-if="imageUrl" class="flex justify-between">
|
<span v-if="imageUrl" class="flex justify-between">
|
||||||
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
|
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
|
||||||
<img :src="imageUrl" class="h-24 rounded-xl" />
|
<img :src="imageUrl" class="h-24 rounded-xl" />
|
||||||
|
|||||||
@@ -277,7 +277,10 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="record.image" class="flex justify-center">
|
<div
|
||||||
|
v-if="record.image"
|
||||||
|
class="flex justify-center"
|
||||||
|
>
|
||||||
<a :href="record.image" target="_blank">
|
<a :href="record.image" target="_blank">
|
||||||
<img :src="record.image" class="h-24 mt-2 rounded-xl" />
|
<img :src="record.image" class="h-24 mt-2 rounded-xl" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ import {
|
|||||||
} from "@/constants/app";
|
} from "@/constants/app";
|
||||||
import { db } from "@/db/index";
|
import { db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { getHeaders } from "@/libs/endorserServer";
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import { base64ToBlob, SHARED_PHOTO_BASE64_KEY } from "@/libs/util";
|
||||||
|
|
||||||
@Component({ components: { PhotoDialog, QuickNav } })
|
@Component({ components: { PhotoDialog, QuickNav } })
|
||||||
export default class SharedPhotoView extends Vue {
|
export default class SharedPhotoView extends Vue {
|
||||||
@@ -86,16 +87,19 @@ export default class SharedPhotoView extends Vue {
|
|||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.activeDid = settings?.activeDid as string;
|
this.activeDid = settings?.activeDid as string;
|
||||||
|
|
||||||
const temp = await db.temp.get("shared-photo");
|
const temp = await db.temp.get(SHARED_PHOTO_BASE64_KEY);
|
||||||
|
const imageB64 = temp?.blobB64 as string;
|
||||||
if (temp) {
|
if (temp) {
|
||||||
this.imageBlob = temp.blob;
|
this.imageBlob = base64ToBlob(imageB64);
|
||||||
|
|
||||||
// clear the temp image
|
// clear the temp image
|
||||||
db.temp.delete("shared-photo");
|
db.temp.delete(SHARED_PHOTO_BASE64_KEY);
|
||||||
|
|
||||||
this.imageFileName = (this.$route as Router).query[
|
this.imageFileName = (this.$route as Router).query[
|
||||||
"fileName"
|
"fileName"
|
||||||
] as string;
|
] as string;
|
||||||
|
} else {
|
||||||
|
console.error("No appropriate image found in temp storage.", temp);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error("Got an error loading an identifier:", err);
|
console.error("Got an error loading an identifier:", err);
|
||||||
@@ -156,7 +160,11 @@ export default class SharedPhotoView extends Vue {
|
|||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
// send the image to the server
|
// send the image to the server
|
||||||
const headers = await getHeaders(this.activeDid);
|
const token = await accessToken(this.activeDid);
|
||||||
|
const headers = {
|
||||||
|
Authorization: "Bearer " + token,
|
||||||
|
// axios fills in Content-Type of multipart/form-data
|
||||||
|
};
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append(
|
formData.append(
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<h2 class="text-xl font-bold mb-4">Image Sharing</h2>
|
<h2 class="text-xl font-bold mb-4">Image Sharing</h2>
|
||||||
Populates the "shared-photo" view as if they used "share_target".
|
Populates the "shared-photo" view as if they used "share_target".
|
||||||
<input type="file" @change="uploadFile" />
|
<input type="file" data-testid="fileInput" @change="uploadFile" />
|
||||||
<router-link
|
<router-link
|
||||||
v-if="showFileNextStep()"
|
v-if="showFileNextStep()"
|
||||||
:to="{
|
:to="{
|
||||||
@@ -165,6 +165,7 @@
|
|||||||
query: { fileName },
|
query: { fileName },
|
||||||
}"
|
}"
|
||||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
||||||
|
data-testid="fileUploadButton"
|
||||||
>
|
>
|
||||||
Go to Shared Page
|
Go to Shared Page
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -257,8 +258,10 @@ import {
|
|||||||
} from "@/libs/crypto/vc/passkeyDidPeer";
|
} from "@/libs/crypto/vc/passkeyDidPeer";
|
||||||
import {
|
import {
|
||||||
AccountKeyInfo,
|
AccountKeyInfo,
|
||||||
|
blobToBase64,
|
||||||
getAccount,
|
getAccount,
|
||||||
registerAndSavePasskey,
|
registerAndSavePasskey,
|
||||||
|
SHARED_PHOTO_BASE64_KEY,
|
||||||
} from "@/libs/util";
|
} from "@/libs/util";
|
||||||
|
|
||||||
const inputFileNameRef = ref<Blob>();
|
const inputFileNameRef = ref<Blob>();
|
||||||
@@ -320,12 +323,13 @@ export default class Help extends Vue {
|
|||||||
const blob = new Blob([new Uint8Array(data)], {
|
const blob = new Blob([new Uint8Array(data)], {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
});
|
});
|
||||||
|
const blobB64 = await blobToBase64(blob);
|
||||||
this.fileName = file.name as string;
|
this.fileName = file.name as string;
|
||||||
const temp = await db.temp.get("shared-photo");
|
const temp = await db.temp.get(SHARED_PHOTO_BASE64_KEY);
|
||||||
if (temp) {
|
if (temp) {
|
||||||
await db.temp.update("shared-photo", { blob });
|
await db.temp.update(SHARED_PHOTO_BASE64_KEY, { blobB64 });
|
||||||
} else {
|
} else {
|
||||||
await db.temp.add({ id: "shared-photo", blob });
|
await db.temp.add({ id: SHARED_PHOTO_BASE64_KEY, blobB64 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
40
test-playwright/35-record-gift-from-image-share.spec.ts
Normal file
40
test-playwright/35-record-gift-from-image-share.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('Record item given from image-share', async ({ page }) => {
|
||||||
|
|
||||||
|
let randomString = Math.random().toString(36).substring(2, 8);
|
||||||
|
|
||||||
|
// Combine title prefix with the random string
|
||||||
|
const finalTitle = `Gift ${randomString} from image-share`;
|
||||||
|
|
||||||
|
// Create new ID using seed phrase "rigid shrug mobile…"
|
||||||
|
await page.goto('./start');
|
||||||
|
await page.getByText('You have a seed').click();
|
||||||
|
await page.getByPlaceholder('Seed Phrase').fill('rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage');
|
||||||
|
await page.getByRole('button', { name: 'Import' }).click();
|
||||||
|
|
||||||
|
// Record something given
|
||||||
|
await page.goto('./test');
|
||||||
|
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||||
|
await page.getByTestId('fileInput').click();
|
||||||
|
const fileChooser = await fileChooserPromise;
|
||||||
|
await fileChooser.setFiles(path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png'));
|
||||||
|
await page.getByTestId('fileUploadButton').click();
|
||||||
|
|
||||||
|
// on shared photo page, choose the gift option
|
||||||
|
await page.getByRole('button').filter({ hasText: /gift/i }).click();
|
||||||
|
|
||||||
|
await page.getByTestId('imagery').getByRole('img').isVisible();
|
||||||
|
await page.getByPlaceholder('What was received').fill(finalTitle);
|
||||||
|
await page.getByRole('spinbutton').fill('2');
|
||||||
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
|
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||||
|
|
||||||
|
|
||||||
|
// Refresh home view and check gift
|
||||||
|
await page.goto('./');
|
||||||
|
const item1 = page.locator('li').filter({ hasText: finalTitle });
|
||||||
|
await expect(item1.getByRole('img')).toBeVisible();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user