Browse Source

fix error sharing image and failing to upload, fix upload in webkit/safari, and test it

Trent Larson 4 months ago
parent
commit
b8181f6ae3
  1. 3
      playwright.config-local.ts
  2. 1
      src/components/PhotoDialog.vue
  3. 3
      src/db/tables/temp.ts
  4. 33
      src/libs/util.ts
  5. 2
      src/views/GiftedDetails.vue
  6. 5
      src/views/HomeView.vue
  7. 18
      src/views/SharedPhotoView.vue
  8. 12
      src/views/TestView.vue
  9. 40
      test-playwright/35-record-gift-from-image-share.spec.ts

3
playwright.config-local.ts

@ -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 */
/** /**

1
src/components/PhotoDialog.vue

@ -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) {

3
src/db/tables/temp.ts

@ -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
}; };
/** /**

33
src/libs/util.ts

@ -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

2
src/views/GiftedDetails.vue

@ -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" />

5
src/views/HomeView.vue

@ -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>

18
src/views/SharedPhotoView.vue

@ -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",

12
src/views/TestView.vue

@ -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

@ -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();
});
Loading…
Cancel
Save