Skip to content

Commit

Permalink
Improves coordinate validation and context (#47)
Browse files Browse the repository at this point in the history
* switching to official json-bigint to fix search test issues

* improving cone search coord and context

* improving search form coords

* cleanup

* cleaning up coord/id validation rules

* expand first about panel

* test mocking axios

* test mocking axios
  • Loading branch information
havok2063 authored Aug 6, 2024
1 parent b9f70c4 commit 5eea98d
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 29 deletions.
17 changes: 12 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"aladin-lite": "^3.3.3-beta",
"axios": "^1.6.0",
"core-js": "^3.29.0",
"json-big": "^1.0.2",
"json-bigint": "^1.0.0",
"pinia": "^2.0.0",
"pinia-plugin-persistedstate": "^3.2.1",
"roboto-fontface": "*",
Expand All @@ -34,6 +34,7 @@
"@pinia/testing": "^0.1.0",
"@playwright/test": "^1.33.0",
"@types/jsdom": "^21.1.1",
"@types/json-bigint": "^1.0.4",
"@types/node": "^20.0.0",
"@types/webfontloader": "^1.6.35",
"@vitejs/plugin-vue": "^5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/axios.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from 'axios'
import JSONbig from 'json-big'
import JSONbig from 'json-bigint'

// Defines the API instance in Axios with special handling for big integers.
const axiosInstance = axios.create({
Expand Down
22 changes: 17 additions & 5 deletions src/components/ConeSearch.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<template>
<v-form ref="form">
<v-text-field class="pt-2"
label="Quick Cone Search"
v-model="searchQuery"
placeholder="315.014, 35.299, 0.1d"
hint="Enter a RA, Dec, radius"
hint="Enter an RA, Dec, and optional radius"
:rules="validationRules"
@input="validate"
clearable
validate-on-blur="true"
@keyup.enter="onSearch"
@click:clear="onClear"
>
<template v-slot:prepend>
<v-icon icon='mdi-help' size='small'
v-tippy="{content:'Enter a RA, Dec coordinate and optional radius, in format: [ra],[dec],[number][d/m/s]',
v-tippy="{content:'Enter a RA, Dec coordinate (degrees), and optional radius, in format: [ra],[dec],[number][d/m/s]. Default radius is 0.1 degree.',
placement:'left'}"></v-icon>
</template>
<template v-slot:append-inner>
Expand All @@ -24,6 +26,7 @@
</v-btn>
</template>
</v-text-field>
</v-form>
</template>

<script lang="ts" setup>
Expand All @@ -45,8 +48,11 @@ const validationRules = [
];
// parameters
let form = ref(null)
let isValid = ref(false)
let loading = ref(false)
let regex = /^(\d+(?:\.\d+)?|\d{1,2}d\s\d{1,2}m\s\d{1,2}(?:\.\d+)?s)(,|\s)+([+-]?\d+(?:\.\d+)?|[+-]?\d{1,2}h\s\d{1,2}m\s\d{1,2}(?:\.\d+)?s)(?:(,|\s)+(\d+(?:\.\d+)?[dms]))?$/
function validate() {
// check the validation rules
Expand All @@ -55,10 +61,16 @@ function validate() {
function validateInput(value: string): boolean {
// validate the input string
const regex = /^(\d+(?:\.\d+)?|\d{1,2}d\s\d{1,2}m\s\d{1,2}(?:\.\d+)?s),\s*([+-]?\d+(?:\.\d+)?|[+-]?\d{1,2}h\s\d{1,2}m\s\d{1,2}(?:\.\d+)?s)(?:,\s*(\d+(?:\.\d+)?[dms]))?$/;
return regex.test(value);
}
function onClear() {
// clear the search query
searchQuery.value = '';
isValid.value = false;
form.value.reset();
}
async function onSearch(): Promise<void> {
// perform the search
Expand All @@ -73,10 +85,10 @@ async function onSearch(): Promise<void> {
function parseInput(input: string): [string, string, { radius: number, units: string }] | [] {
// parse the input string into RA, Dec, and radius
const match = input.match(/^(\d+(?:\.\d+)?|\d{1,2}d\s\d{1,2}m\s\d{1,2}(?:\.\d+)?s),\s*([+-]?\d+(?:\.\d+)?|[+-]?\d{1,2}h\s\d{1,2}m\s\d{1,2}(?:\.\d+)?s)(?:,\s*(\d+(?:\.\d+)?[dms]))?$/);
const match = input.match(regex);
if (!match) return [];
let [ra, dec, radiusStr = "0.1d"] = match.slice(1);
let [ra, gap1, dec, gap2, radiusStr = "0.1d"] = match.slice(1);
let units = 'degree';
if (radiusStr.endsWith('m')) {
units = 'arcmin';
Expand Down
3 changes: 2 additions & 1 deletion src/views/About.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<!-- How to Use Section -->
<v-row>
<v-col cols="12">
<v-expansion-panels :value="0">
<v-expansion-panels v-model="expanded">
<v-expansion-panel
title="Getting Started"
text="Basic instructions on getting started with the website.">
Expand Down Expand Up @@ -110,6 +110,7 @@ useStoredTheme()
const theme = useTheme()
const expanded = ref([0]) // Start with the first panel expanded
const resources = ref([
{
title: 'Data Access',
Expand Down
73 changes: 59 additions & 14 deletions src/views/Search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
hint="Enter a RA, Dec coordinate in [decimal or hmsdms] format"
:rules="coordRules"
id="coords"
:disabled="coordsDisabled"
/>
</v-col>
<v-col md=4>
Expand All @@ -24,6 +25,7 @@
hint="Enter a search radius"
:rules="radiusRules"
id="radius"
:disabled="coordsDisabled"
/>
</v-col>
<v-col md=2>
Expand All @@ -33,6 +35,7 @@
label="Unit"
id="unit"
:items="['degree', 'arcmin', 'arcsec']"
:disabled="coordsDisabled"
></v-select>
</v-col>
</v-row>
Expand All @@ -46,8 +49,10 @@
hint="Enter an SDSS identifier"
:rules="idRules"
id="id"
:disabled="idDisabled"
/>
</v-col>
<!-- cartons and programs dropdown menus -->
<v-col cols="4" md="4">
<dropdown-select label="Programs" id="programs" :items="store.programs" v-model="formData.program"/>
</v-col>
Expand All @@ -57,7 +62,8 @@
</v-row>

<v-row>
<v-col cols="12">
<v-col cols="3">
<!-- observed targets toggle -->
<v-switch
v-tippy="{content:'Toggle between only observed targets or all targets', placement: 'left', maxWidth:200}"
v-model="formData.observed"
Expand All @@ -71,6 +77,7 @@

<v-row>
<v-col cols="4">
<!-- search button -->
<v-btn rounded="lg" color='primary' @click="submit_form" size="large" :disabled="!valid" :append-icon="valid ? 'mdi-check-circle' : 'mdi-close-circle'">Search
<template v-slot:append>
<v-icon size='large' :color="!valid ? 'error' : 'success'"></v-icon>
Expand All @@ -79,10 +86,12 @@
</v-btn>
</v-col>
<v-col cols="4">
<!--revalidate search form -->
<v-btn color="warning" rounded="lg" @click="revalidate" size="large">Revalidate
</v-btn>
</v-col>
<v-col cols="4">
<!-- reset form -->
<v-btn rounded="lg" @click="reset_form" size="large">Reset</v-btn>
</v-col>

Expand Down Expand Up @@ -115,37 +124,35 @@ let form = ref(null);
// set up validation rules
const exclusiveFieldRule = () => {
const coordsFilled = !!formData.value.coords.trim()
const idFilled = !!formData.value.id.trim()
const good = (coordsFilled && !idFilled) || (!coordsFilled && idFilled)
return good || 'Either Coordinate or ID must be filled, but not both.'
};
// coordinate regex
const re = /([0-9.hms\s:]+),\s*([+-]?[0-9.dms\s:]+)/gm;
const re = /([0-9.hms\s:]+)(,|\s)+([+-]?[0-9.dms\s:]+)/gm;
let coordRules = [
// (value: string) => !!value || 'Required field.',
exclusiveFieldRule,
(value: string) => {
if (!value) return true
return value.match(re) != null || 'Value does not match sky coordinate regex'
},
]
let idRules = [
(value: number) => !isNaN(value) || 'Value must be a number.',
]
let radiusRules = [
(value: number) => !isNaN(value) || 'Value must be a number.',
]
let idRules = [exclusiveFieldRule] //[(value: number) => !!value || 'Required field.']
// parameters
let loading = ref(false)
let coordsDisabled = ref(false)
let idDisabled = ref(false)
// create initial state of formData
let initFormData = {
coords: '',
ra: '',
dec: '',
radius: '',
radius: '0.1',
id: '',
units: 'degree',
release: store.release,
Expand All @@ -163,6 +170,12 @@ let filteredCartons = ref([])
// create watcher for the form validation
watch(formData, async () => {
console.log('in watcher validate')
// update id/coords disabled states
idDisabled.value = !!formData.value.coords.trim()
coordsDisabled.value = !!formData.value.id.trim()
const formValid = await form.value.validate(); // validate the form
valid.value = formValid.valid; // update the valid state
}, { deep: true }); // deep watch to track nested property changes
Expand All @@ -185,7 +198,7 @@ async function submit_form(this: any) {
}
// extract out ra and dec fields from coords
[formData.value.ra, formData.value.dec] = formData.value.coords ? formData.value.coords.split(',') : ["", ""]
[formData.value.ra, formData.value.dec] = extract_coords(formData.value.coords)
console.log('submitting', formData.value)
await axiosInstance.post('/query/main',
Expand Down Expand Up @@ -228,6 +241,36 @@ async function submit_form(this: any) {
})
}
function extract_coords(coords: string) {
// extract ra, dec coordinates from the input string and format them
let [ra, dec] = ["", ""]
if (coords.includes(',')) {
[ra, dec] = coords.split(',')
} else if (coords.includes(' ')) {
let obj = coords.split(' ')
switch (obj.length) {
case 2:
// [ra, dec]
[ra, dec] = obj
break
case 3:
// [ra, "", dec]
[ra, dec] = [obj[0], obj[2]]
case 6:
// [hh, mm, ss, dd, mm, ss]
[ra, dec] = [obj.slice(0, 3).join(' '), obj.slice(3, 6).join(' ')]
break
default:
[ra, dec] = obj
}
} else {
[ra, dec] = ["", ""]
}
return [ra.trim(), dec.trim()]
}
async function revalidate() {
// revalidate the search form
const formValid = await form.value.validate()
Expand All @@ -247,6 +290,8 @@ async function reset_form() {
fail.value = false
failmsg.value = ''
loading.value = false
idDisabled.value = false
coordsDisabled.value = false
// Reset the form validation state
await form.value.resetValidation();
Expand Down
56 changes: 56 additions & 0 deletions src/views/__tests__/Search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'

import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'

// must load this if component uses pinia store, even if not directly used in test
import { createTestingPinia } from '@pinia/testing'

import Search from '../Search.vue'

// Mock the specific axiosInstance
vi.mock('@/axios', () => ({
default: {
get: vi.fn(() => Promise.resolve({ data: 'mocked data' })),
post: vi.fn(),
// Add other methods as needed
},
}));

describe('Search', () => {
const vuetify = createVuetify({ components, directives })
const pinia = createTestingPinia()


let wrapper

beforeEach(() => {
wrapper = mount(Search, {
global: {
plugins: [vuetify, pinia]
},
props: {
files: []
}
})
})

it.each([
['123.45,67.89', '123.45', '67.89'],
['123.45, -67.89', '123.45', '-67.89'],
['123.45 +67.89', '123.45', '+67.89'],
['21:00:33.6 +25:17:56.4', '21:00:33.6', '+25:17:56.4'],
['21 00 33.6 25 17 56.4', '21 00 33.6', '25 17 56.4'],
['21h03m07.2s, -03d12m00s', '21h03m07.2s', '-03d12m00s'],
['21h 03m 07.2s -03d 12m 00s', '21h 03m 07.2s', '-03d 12m 00s']
])('splits coordinates "%s" into ra and dec', (coords, expRa, expDec) => {
const extract_coords = wrapper.vm.extract_coords
const [ra, dec] = extract_coords(coords)
expect(ra).toBe(expRa)
expect(dec).toBe(expDec)
})


})
Loading

0 comments on commit 5eea98d

Please sign in to comment.